TOC BACK FORWARD HOME

UNIX Unleashed, Internet Edition

- 25 -

Introduction to CVS

by Fred Trimble

Like RCS and SCCS, CVS is a source code version control utility. It is one of the many excellent tools available from the GNU Software Foundation. CVS, which is an acronym for Concurrent Versions System, was originally developed by Dick Grune in 1986. When it was first developed, it consisted of a set of UNIX shell scripts. In 1989, it was designed and coded in the C language by Brian Berlinger, with some enhancements provided by Jeff Polk. Many of the algorithms in the current version came from the original shell scripts.

You can think of CVS as a front-end tool to RCS. It stores version-control information for each file in RCS format. (For more information on RCS, see Chapter 24, "Introduction to RCS.") RCS files are used in conjunction with the diff command to provide a robust version control system.

Currently, the most recent version of CVS is version 1.9. You can obtain it from many sites via anonymous FTP, including prep.ai.mit.edu under the pub/gnu directory. You need compatible versions of RCS, as well as the diff command, for CVS, and you can them there as well. In addition, you can subscribe to a mailing list devoted to CVS by sending the word subscribe in the message body to info-cvs@prep.ai.mit.edu.

How Is CVS Different from RCS and SCCS?

Both RCS and SCCS use a lock-modify-unlock paradigm for managing changes to a file. When a developer wants to change a file, he or she must first lock the file. Other developers cannot check out the file for modification until the file has been unlocked by the original developer. Although this model is very effective in maintaining and managing the contents of a file, it may cause unnecessary delay in situations in which more than one person wants to work on different portions of the same file concurrently.

Instead of serializing access to files under source code control, CVS supports simultaneous access and modification of files using a copy-modify-merge paradigm. Here, a user can check out a file and make modifications even though another person may be in the process of modifying it as well. Every time a file is checked in, it is merged with the most recent copy in the CVS repository. On rare occasions, a merge cannot be done because of conflicting entries in the files to be merged. In this case, conflict resolution needs to be performed. I discuss how to handle conflicts later in this chapter. I should emphasize that such instances should be rare in a well-organized project.

Starting a Project

The first step in using CVS to manage your source code is to create a repository. A repository is simply a directory hierarchy containing the source code to be managed and various administrative files that manage the source code. To create a repository, first set the environment variable $CVSROOT to point to the absolute pathname of the repository. Before you issue the command, make sure that the directory exists. Then issue the cvs command with the init option. For example, the following commands check the setting of CVSROOT, verify the existence of the repository root directory, and then create the repository:

$ echo $CVSROOT
/usr/local/cvsroot
$ ls -ld $CVSROOT
drwxrwxrwx 4 trimblef users    1024 May 17 14:55 /usr/local/cvsroot
$ cvs init
$

The cvs command does its work silently, creating an administrative directory hierarchy in the /usr/local/cvsroot directory. You can override the setting of the $CVSROOT environment variable by using the -d option. For example, the following command initializes a CVS repository under /usr/cvsroot, even though the $CVSROOT environment variable points to a different location:

cvs -d /usr/cvsroot init

In fact, you might notice that many cvs commands allow you to specify the root directory of the repository in this fashion.

The repository is now ready to manage your source code. Make sure that your developers point to the appropriate repository by setting the $CVSROOT environment variable in their environments, or use the -d option on the cvs command line.

The Repository

As I mentioned, the repository contains administrative files in addition to the source code under control. The administrative files portion of the hierarchy does not have to exist in order for CVS to function. However, these files support many useful features, and leaving them in place is highly recommended. Figure 25.1 shows the files that are created under the $CVSROOT/CVSROOT directory after cvs init is invoked.

Figure 25.1
Contents of the CVSROOT directory after repository initialization.

Notice that some of the files have an extension of ,v. It is the default file extension for files under RCS control. Indeed, these are RCS files, and they illustrate how CVS is used as a front end to RCS. In addition to the actual source files, the administrative files can be checked out and modified using the appropriate cvs commands as well. Following is a description of the purpose for each of the files in the repository:

checkoutlist This file supports other administrative files in CVSROOT. It allows you to customize diagnostic messages for various cvs commands.
commitinfo This file specifies programs that should be executed when a cvs commit command is executed. This way, you can perform a sanity check on the files before they are entered into the repository. The contrib directory that comes with the CVS distribution contains a number of useful sample scripts.
cvswrappers This file defines wrapper programs that are executed when files are checked in or out. One possible use of this file is to format checked-in source code files so that their appearance and structure are consistent with other files in the repository.
editinfo This file allows you to execute a script before a commit starts but after log information has been recorded. If the script exits with a non-zero value, the commit is aborted.
history This file keeps track of all commands that affect the repository.
loginfo The loginfo file is similar to commitinfo. The major difference is that loginfo is processed after files have been committed. Typical uses of this file include sending electronic mail and appending log messages to a file after a commit takes place.
modules This file enables you to define a symbolic name for a group of files. If this is not done, you must specify a partial pathname, relative to the $CVSROOT directory, for each file that you reference.
notify This file controls notifications from watches set by the cvs watch add and cvs edit commands.
rcsinfo This file allows you to specify a template for a commit log session.
taginfo This file defines programs to execute after any tag operation. For example, if a tag name changes, you can configure the file to send a mail message to the original developer, notifying him or her that the file has changed.

Importing Files into the Repository

Now that the repository is initialized, you can add files and directories of files. You do so by using the import command. For example, suppose you have the following source code file hierarchy that is ready to be put under revision control:

$ find . -print
.
./src
./src/main
./src/main/main.c
./src/main/main.h
./src/print
./src/print/print.c
./src/print/print.h
./src/term
./src/term/term.c
./src/term/term.h
$

From the directory containing the src directory, run the following command:

$ cvs import -m "initial release" project myvtag myrtag
cvs import: Importing /usr/local/cvsroot/project/src
cvs import: Importing /usr/local/cvsroot/project/src/main
N project/src/main/main.c
N project/src/main/main.h
cvs import: Importing /usr/local/cvsroot/project/src/print
N project/src/print/print.c
N project/src/print/print.h
cvs import: Importing /usr/local/cvsroot/project/src/term
N project/src/term/term.c
N project/src/term/term.h
No conflicts created by this import
$

In the preceding command, the -m option gives a description of the import for logging purposes. The next option, project, identifies a directory under the $CVSROOT directory that will contain the imported source files. The next two arguments identify the vendor tag and release tag, respectively.

After the files have been imported, you can create abbreviations to the source code in the directory hierarchy. Doing so makes checking files out of the repository easier. This process is discussed in the section entitled Checking Out Files.

File Permissions

After source files have been entered into the repository, the source code administrator can control access by setting appropriate file and directory permissions. All RCS files (files that end with ,v) are created with read-only access. They should never be changed. The directories in the repository should have write permissions by users who are allowed to modify the files in the directory. Therefore, you cannot control file access on a file-by-file basis. You can control access only to files at the group level within a directory.

Maintaining Source Code Revisions

After the source files have been imported, there are a variety of options available for the cvs command for managing the repository. These are discussed in the following sections.

Checking Out Files

Now that the source files have been imported, you can check files in and out as needed to make the appropriate modifications. The cvs command has an option to check out a file from the repository. For example, you can use the following command to get all the files from the print directory:

$ cvs checkout project/src/print
cvs checkout: Updating project/src/print
U project/src/print/print.c
U project/src/print/print.h
$

The partial pathname project/src/print specified in the command line is relative to the $CVSROOT directory. The command creates the same partial path under your current working directory, including the source files:

$ find . -print
.
./project
./project/CVS
./project/CVS/Root
./project/CVS/Repository
./project/CVS/Entries
./project/CVS/Entries.Static
./project/CVS/Entries.Log
./project/src
./project/src/CVS
./project/src/CVS/Root
./project/src/CVS/Repository
./project/src/CVS/Entries
./project/src/CVS/Entries.Static
./project/src/CVS/Entries.Log
./project/src/print
./project/src/print/CVS
./project/src/print/CVS/Root
./project/src/print/CVS/Repository
./project/src/print/CVS/Entries
./project/src/print/print.c
./project/src/print/print.h
$

Notice that in addition to the desired files being copied from the repository, a directory named CVS is also created. It is for CVS administrative purposes only, and its contents should never be modified directly. When you want to make modifications to the checked-out files, change to the project/src/print directory. After you make the appropriate changes to all the checked-out files, you can commit the changes to the repository by issuing the cvs commit command. This issue is discussed in the next section.

Specifying the partial path to a directory in the repository can be cumbersome, especially in a large project with many directory levels. In CVS, you can create abbreviations for each of the source directories. You do so by configuring the modules file in the $CVSROOT/CVSROOT directory. To do so, execute the following command to "check out" a copy of the modules file:

$ cvs checkout CVSROOT/modules
U CVSROOT/modules
$

This command creates a CVSROOT directory in your local working directory. After you change to this directory, you see a copy of the modules file that you can edit. For the preceding example, add the following lines to the end of the file:

src     project/src
main    project/src/main
print   project/src/print
term    project/src/term

For the changes to take affect, you must "commit" them. You do so by using the cvs commit command, as follows:

$ cvs commit -m "initialize modules"
initialized the modules file
Checking in modules;
/users/home/project/CVSROOT/modules,v  <--  modules
new revision: 1.2; previous revision: 1.1
done
cvs commit: Rebuilding administrative file database
$

(The cvs commit command is discussed more fully in the next section.)

This action enables you to select the modules in a directory without having to specify the entire path. For example, suppose you want to select the files that comprise the print module. Instead of specifying project/src/print on the command line, you can use print instead:

$ cvs checkout print
cvs checkout: Updating print
U print/print.c
U print/print.h
$ cd print
$ ls -l
total 6
drwxrwxrwx   2 trimblef users       1024 May 25 10:47 CVS
-rw-rw-rw-   1 trimblef users         16 May 25 10:26 print.c
-rw-rw-rw-   1 trimblef users         16 May 25 10:26 print.h
$

When you check files out of the repository, you get the most recent revision by default. You can specify another revision if, for example, you need to patch an earlier version of the source code. Suppose that the current revision for the print module is 1.4. You can get the 1.1 revision by using the -r option, as follows:

$ cvs checkout -r 1.1 print
cvs checkout: Updating print
U print/print.c
U print/print.h
$

For a complete list of all the options you can use with a particular cvs command, use the -H option. For example, here is how you list all the options for the checkout command:

$ cvs -H checkout
Usage:
  cvs checkout [-ANPcflnps] [-r rev | -D date] [-d dir] [-k kopt] modules...
        -A      Reset any sticky tags/date/kopts.
        -N      Don't shorten module paths if -d specified.
        -P      Prune empty directories.
        -c      "cat" the module database.
        -f      Force a head revision match if tag/date not found.
        -l      Local directory only, not recursive
        -n      Do not run module program (if any).
        -p      Check out files to standard output (avoids stickiness).
        -s      Like -c, but include module status.
        -r rev  Check out revision or tag. (implies -P) (is sticky)
        -D date Check out revisions as of date. (implies -P) (is sticky)
        -d dir  Check out into dir instead of module name.
        -k kopt Use RCS kopt -k option on checkout.
        -j rev  Merge in changes made between current revision and rev.
$

Checking In Files

After you check out files from the repository and make the changes you want, you can check in the modified file(s) by using the cvs commit command. For example, you can use the following command to check in the modified files from the preceding print example:

$ cvs commit -m "update print code"
cvs commit: Examining .
cvs commit: Committing .
Checking in print.c;
/users/home/project/src/print/print.c,v  <--  print.c
new revision: 1.2; previous revision: 1.1
done
$

In this example, the output indicates that only the file print.c has changed. Also, note that the original files are imported as revision 1.1. After the commit operation, a revision 1.2 also exists. Both of these revisions are on the main trunk of the repository. When a check-out operation is done, you can specify which revision to fetch. This way, you can patch previous versions of the source code.

The -m option allows you to log comments about the commit. This capability is useful for keeping track of the reason that the code was modified in the first place. If you don't specify the -m option, the editor specified in the CVSEDITOR environment variable is invoked (vi is the default), along with the following help text:

CVS: ----------------------------------------------------------------------
CVS: Enter Log.  Lines beginning with ´´CVS: ´´ are removed automatically
CVS:
CVS: ----------------------------------------------------------------------

This text reminds you to log comments before committing your changes.

Updates

As I stated earlier, CVS allows one or more persons to modify a file at the same time. Suppose you are modifying one section of a file, and you want to update your local checked-out copy to incorporate changes made by another user who has checked the file in to the repository. You can do so by using the update command. This command, which is considered among the most heavily used cvs commands, has many options.

As a simple example, to merge changes made by others with your local working copies, you can invoke cvs with the update option:

$ cvs update
cvs update: Updating project
cvs update: Updating project/project
cvs update: Updating project/project/src
cvs update: Updating project/project/src/main
cvs update: Updating project/project/src/print
cvs update: Updating project/project/src/term
$

Branches

Branches enable you to make modifications to some of the files without disturbing the main trunk (For a generic treatment of branches and other source code control concepts, see Chapter 23.)

The first step in creating a branch is to create a tag for some of the files in the repository. A tag is simply a symbolic name given to a file or group of files. The same tag name is usually given to a set of files that comprise a module at a strategic point in the life cycle of the source code (such as a patch or when a release is made). To create a tag, run the cvs tag command in your working directory. For example, the following command tags the src directory:

$ cvs checkout src
cvs checkout: Updating src
cvs checkout: Updating src/main
U src/main/main.c
U src/main/main.h
cvs checkout: Updating src/print
U src/print/print.c
U src/print/print.h
cvs checkout: Updating src/term
U src/term/term.c
U src/term/term.h
leibniz 34: cvs tag release-1-0
cvs tag: Tagging src
cvs tag: Tagging src/main
T src/main/main.c
T src/main/main.h
cvs tag: Tagging src/print
T src/print/print.c
T src/print/print.h
cvs tag: Tagging src/term
T src/term/term.c
T src/term/term.h
$

In the next step, you use the tag you just created to create a branch. To do so, you use the rtag command, as follows:

$ cvs rtag -b -r release-1-0 release-1-0-patches print
cvs rtag: Tagging project/src/print
$

To see the current state of your local working copy of files, including the branch you are currently working on, use the status option to cvs:

leibniz 38: cvs status -v src
cvs status: Examining src
cvs status: Examining src/main
===================================================================
File: main.c            Status: Up-to-date
   Working revision:    1.2     Sun May 25 14:45:24 1997
   Repository revision: 1.2     /users/home/project/src/main/main.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0                     (revision: 1.2)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)
===================================================================
File: main.h            Status: Up-to-date
   Working revision:    1.1.1.1 Sun May 25 14:26:40 1997
   Repository revision: 1.1.1.1 /users/home/project/src/main/main.h,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0                     (revision: 1.1.1.1)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)
cvs status: Examining src/print
===================================================================
File: print.c           Status: Up-to-date
   Working revision:    1.3     Mon May 26 06:10:25 1997
   Repository revision: 1.3     /users/home/project/src/print/print.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0-patches             (branch: 1.3.2)
        release-1-0                     (revision: 1.3)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)
===================================================================
File: print.h           Status: Up-to-date
   Working revision:    1.1.1.1 Sun May 25 14:26:41 1997
   Repository revision: 1.1.1.1 /users/home/project/src/print/print.h,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0-patches             (branch: 1.1.1.1.2)
        release-1-0                     (revision: 1.1.1.1)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)
cvs status: Examining src/term
===================================================================
File: term.c            Status: Up-to-date
   Working revision:    1.2     Mon May 26 15:02:14 1997
   Repository revision: 1.2     /users/home/project/src/term/term.c,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0                     (revision: 1.2)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)
===================================================================
File: term.h            Status: Up-to-date
   Working revision:    1.1.1.1 Sun May 25 14:26:41 1997
   Repository revision: 1.1.1.1 /users/home/project/src/term/term.h,v
   Sticky Tag:          (none)
   Sticky Date:         (none)
   Sticky Options:      (none)
   Existing Tags:
        release-1-0                     (revision: 1.1.1.1)
        myrtag                          (revision: 1.1.1.1)
        myvtag                          (branch: 1.1.1)

Note that in this example, the print module is currently on branch 1.1.1. I discuss the process of merging a branch with the main trunk of development in the next section.

Merging

You can merge the changes made on a branch to your local working copy of files by using the -j option of the cvs update command:

$ cvs update -j release-1-0 print.c

Using this command, you can merge the latest version of print.c from the main trunk with the modifications performed in release-1-0. After the next commit, the changes will be incorporated in the main trunk.

Conflict Resolution

Because more than one developer can check out and modify a file at a time, conflicts can result. For example, suppose you have just completed work on revision 1.4 of a file and run the update command:

$ cvs update print.c
RCS file: /users/home/project/src/print/print.c,v
retrieving revision 1.4
retrieving revision 1.7
Merging differences between 1.4 and 1.7 into print.c
rcsmerge warning: overlaps during merge
cvs update: conflicts found in print.c
C print.c

These messages are printed when conflicting changes are made to a common section of the source file. You must handle these conflicts manually. In this example, the local copy of the print.c file is saved in the file .#print.c.1.4. The new local version of print.c has the following contents:

#include <stdio.h>
int print(char *args[])
{
   if (parse(args) < 1)
   {
<<<<<<< print.c
      fprintf(stderr, "Invalid argument list.\n");
=======
      fprintf(stderr, "No arguments present.\n");
>>>>>>> 1.7
   }
   ...
}

Note how the conflicting entries are clearly marked with <<<<<<<, =======, and >>>>>>>. You need to resolve this section of code manually. After consulting the developer responsible for the conflicting update and making the appropriate change to the file, you can commit the change with the following command:

$ cvs commit -m "fix print module diagnostic" print.c
Checking in print.c
/usr/local/cvsroot/project/src/print.c,v  <-- print.c
new revision: 1.8; previous revision: 1.7
done

Cleaning Up

After making the necessary modifications to the source files, suppose you decide to remove your working copies. One way is to simply remove the files, as follows:

% rm -r src

The preferred method, however, is to use the release command. It indicates to other developers that the module is no longer in use. Consider this example:

% cvs release -d print
M print.c
You have [1] altered files in this repository.
Are you sure you want to release (and delete) module ´´print.c´´: n
** ´´release´´ aborted by user choice.

In this example, CVS noticed that the local copy of print.c is different from the one in the repository. Therefore, modifications have been made since the last time this file was committed. Checking whether the file needs to be committed before your working copy is removed is good practice.

Keywords

The cvs status and cvs log commands provide useful information on the state of your local copy of files. Another useful technique for managing files is a mechanism known as keyword substitution. Its operation is simple: every time a cvs commit operation is performed, certain keywords in the source files are expanded to useful values. These keywords actually come from the underlying RCS commands. Here is a list of the available keywords, along with descriptions:
$Author$ The login name of the user that checked in the revision.
$Date$ The date that the revision was checked in.
$Header$ The standard header containing the full pathname of the RCS file, date, and author.
$Id$ The same as $Header$, except that the RCS filename does not include the full path.
$Log$ This keyword includes the RCS filename, revision number, author, date, and the log message supplied during commit.
$RCSfile$ The name of the RCS file, not including the path.
$Revision$ The revision number that has been assigned.
$Source$ The full pathname of the RCS file.
$State$ The state that has been assigned to the revision. The state is assigned with the cvs admin -s command. See the admin option for more details.

For example, suppose the following line of text appears at the beginning of a C program file before it has been committed to the repository:

static char *rcsid = "$Id$";

After the source file is committed, the string "$Id$" is replaced with the following header:

static char *rcsid = "$Id: term.c,v 1.2 1997/05/26 15:02:14 trimblef Exp $";

The RCS package includes the ident command, which you can use to extract this RCS keyword information from a text or binary file:

$ ident term.c
term.c:
     $Id: term.c,v 1.2 1997/05/26 15:02:14 trimblef Exp $
$

Environment Variables

CVS makes use of many environment variables. Here is a summary of all the available ones, along with brief descriptions of each:

CVSROOT This environment variable should contain the full pathname to the root of the repository. In many cvs commands, you can override its value by using the -d option.
CVSREAD When this variable is set, all files created during the check-out operation are given read-only permissions. If it is not set, you can modify any files that are checked out.
RCSBIN CVS uses many facilities provided by RCS. Therefore, it needs to know that RCS executables such as ci and co can be found.
CVSEDITOR This variable specifies the editor to use when CVS prompts the user for log information.
CVS_RSH CVS uses the contents of this file to indicate the name of the shell to use when starting a remote CVS server.
CVS_SERVER This environment variable determines the name of the cvs server command. The default is cvs.
CVSWRAPPERS This variable is used by the cvswrappers script to determine the name of the wrapper file.

Summary

Both RCS and SCCS employ a lock-modify-unlock paradigm for source code control. While this is sufficient for certain types of projects, it can impede progress in cases where multiple developers want to modify different parts of a single file simultaneously. CVS supports such parallel development by using a copy-modify-merge paradigm instead. CVS is built on top of RCS, and provides a rich set of options to support and enhance the software development process.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.