Ch 25 -- Introduction to CVS
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.
©Copyright,
Macmillan Computer Publishing. All rights reserved.
|