Using CVS for revision control

Introduction to source code control

As software projects become larger, it makes sense to move to a team model of development, where a number of programmers work together to design, implement, document, test, and maintain a project. As Fred Brooks observed, adding more developers to a project can increase the costs of communication substantially. One way to reduce these costs is for developers to work semi-independently on the code while remaining aware of, and staying up-to-date with, modifications by other team members. Revision control systems like CVS provide tools to help make this method of team development simpler.

Advantages of revision control

CS499 Version Control Activity

Work as teams. For the “Shared access to the repository” step, members of each team have been put into a group:

cs4991    CorrectCare project

cs4992    project/customer update  web application

cs4993    interactive touch screen

cs4994    carpool matching

cs4995    CS115 educational game

 

The “pserver mode” step is skipped.

 

Introduction to CVS and the cvs command

The most basic way to create, administer, and access a CVS repository is through a command-line interface. The command-line interface is accessed through the cvs command. This command is available on most of the Unix, Linux, and Mac OS X machines on campus, including the Multilab, Multilab, SWEB, and many others.

The cvs command: general information

The cvs commands provides a number of different tools for working with source code repositories and working directories. Each such too tool, including update, commit, and log, is a subcommand of cvs. Certain options or flags, starting with a hyphen "-", can be used to change the behavior of these commands.

CVS usage

cvs [global-options] subcommand [command-options]

The permissable command-options depend on the subcommand; see the documentation for details. Global-options, on the other hand, are common to all subcommands. Note that global options must come before the subcommand, while command options must come after it.

The global-options include:

-H

Display help for the subcommand.

-v

Display version information (and do nothing else).

-r

Create files with read-only permissions (default is read-write).

-n

Simulate the operation, but do not actually change the repository.

-d repository

Use the specified repository.

-e editor

Use the specified text editor for writing commit log messages.

Run cvs --help-options for a list of all the global options.

For more information

This tutorial covers only the most basic features of the cvs command. For more details, there are three major sources of information:

The repository

The repository is the location where CVS stores the history of your code, including all previous revisions of each source file, the date and log message of each commit (see the section "Commiting your code to the repository" below), and the user-name of each committer.

The repository is simply a directory. Every cvs command you execute needs to know the location of the repository. This location must be an absolute path, not relative: that is, it must start with / or ~. Furthermore, the repository should not be the same directory as your source code. Typically when you create a repository you will specify a new directory name (that does not already exist). Afterwards, you will use this repository for all your future work. There are three ways to specify the repository location:

~$ cvs -d ~jrandom/CVS/ checkout myproject
·                ~$ CVSROOT=~jrandom/CVS/; export CVSROOT
·                # If you are using csh or tcsh as your shell (in MULTILAB, for example),
·                # instead use the command:
·                #   setenv CVSROOT ~jrandom/CVS/
·                 
·                # Now future cvs commands will use the repository in ~jrandom/CVS/
·                ~$ cvs checkout myproject
~$ cvs status

If you wish to set CVSROOT every time you log in, add the CVSROOT= line to your .profile. If you are using csh or tcsh as your shell (the default in MULTILAB), instead add the setenv to your .login file.

If the repository is specified in multiple ways, the -d argument is given preference, followed by the CVS/Root file. The CVSROOT environment variable is used only if neither of the other methods is provided.

Creating a repository

To create a new repository, use the cvs init subcommand. Remember that you must specify the repository's location with either -d or CVSROOT. If the directory does not yet exist, it will be created. Then CVS will create its administrative files in the directory, marking it as a repository. This subcommand will not overwrite existing files: it is safe, though pointless, to run cvs init on an existing respository. However, you should not use an existing source code directory, or you will be unable to import that directory later.

# Create a repository in the directory "CVS" under my home directory

~$ cvs -d ~/CVS/ init

Shared access to the repository

The permissions of the repository directory and its subdirectories govern who can access the contents. As is usual in Unix, user can look at the contents of a project directory only if the user has:

Furthermore, in order to add or remove contents in a directory, the user must have write access to the directory. Thus creating a new project requires write access to the repository directory, while adding a file to a project requires write access to the project.

With CVS, however, the situation becomes a little more complicated. As part of checking out a project, CVS automatically creates and destroys lock files in the project directory, to make sure other users don't change files while they are being downloaded. Because of this, checking out code from a project requires write access. Thus, normally, everyone who can check out code from a project can also commit.

Furthermore, CVS never overwrites old files directly. Instead, it creates a new copy, swaps them, and removes the original; this way, even if the computer crashes in the middle of an operation, the original file will still be around uncorrupted. To enforce this mode of operation, CVS makes all the files it creates in the repository read-only. In general, with CVS, you should only set the permissions of directories, not ordinary files.

On a small team project, it is usually sufficient to give all the members of the team, and only those members, full access to the repository.

In order to do this, there must be a Unix group to which the members of the team, and only those members, belong. Groups can only be created and changed by the system administrator, so you cannot accomplish this part on your own. Once a group has been created for your team, you will need to know the name of your group. Suppose that your team has the group team123, and that you have created a repository in the directory ~/CVS/.

# First, make the repository and all of its contents belong to team123

~$ chgrp -R team123 ~/CVS/

 

# Give the group full access to the repository and all its subdirectories

~$ find ~/CVS/ -type d -exec chmod g+rwxs ";"

 

# In order to access a file or directory, users must have execute access

# on all the ancestor directories.  As a result, you must grant execute access

# on your home directory.

~$ chmod +x ~/

The second command finds all the directories within the repository, and makes them readable, writable, and executable by the members of the group; thus all the members of the group can create new projects, and check out and commit changes to existing ones. Furthermore, the chmod command sets the "set group ID" (sgid) flag on directories. This causes all new files created within the directory to belong to the directory's group (that is, team123), rather than the committer's default group.

pserver mode

In addition to the mode of operation described above, CVS can also run in "password server" or pserver mode. In pserver mode, a network process runs on the CVS server, and clients (that is, developers working with the project) tell CVS to connect to the server rather than using a particular directory. When running as a password server, CVS performs its own authentication, rather than relying on the operating system's accounts and passwords. Among the advantages of pserver mode are that team members do not have to all have accounts on the same machine, and that the system administrator does not have to create a group for the team. Setting up CVS password server is beyond the scope of this document; for more information, see Section 2.9.3 of the CVS manual.

To connect to a pserver, you must give a special repository name. The format is:

:pserver:user@hostname:/directory

Where hostname is the name of the computer running the pserver (for example, csurs1.csr.uky.edu), directory is the full directory path of the repository on that machine, and user is your username on the pserver. Note that, even if you do have a login on the machine in question, the pserver username is not necessarily the same (but often is). Finally, there may be some situations where you need to use :gserver: instead of :pserver:; the pserver administrator will tell you if this is the case.

Before you issue any other commands, you must log in to the password server. Do this using the login subcommand. This subcommand prompts you for the password, saves it in the file .cvspass in your home directory, and connects to the server. Because the password is saved, you should never need to enter it again when connecting to the same pserver from the same client machine.

Connecting to a CVS pserver

# Set the CVSROOT environment variable to point to the pserver repository

~$ CVSROOT=:pserver:jrandom@csurs.csr.uky.edu:/home/cvsteam1/

~$ export CVSROOT

 

# Log in to the pserver

~$ cvs login

CVS password: Enter your password here

 

# Now you can execute any of the other subcommands on this repository.

Creating or importing a project

Now that you have access to a repository, whether locally or through a remote pserver, it is time to create your first project. You might already have some code to store in the repository. If you do, you can import it using the cvs import subcommand.

cvs import usage

cvs [global-options] import [options] projectname vendor release

Create a CVS project out of the contents of the current directory. If the current directory is the same as the repository, or contains the repository, the command will fail.

-m "message"

Use the specified log message instead of launching an editor.

-I filename

Ignore all files with the specified filename. You may use this option multiple times, each time listing a different filename. It is also possible to specify wildcards with * and ?; if you do so, you must enclose the wildcard in quotes (for example, -I "*.out").

Log messages

When you perform an import (and later, commit), CVS will launch a text editor where you can type a log message. Log messages are described in more detail later; for the first import, something like "Initial import" will probably be sufficient.

CVS uses the EDITOR enviroment variable to determine which text editor to use for log messages. By default, if this environment variable is not set, CVS uses vi as its editor. If you are not comfortable with vi, you may wish to change this to another editor such as emacs, joe, or nano. You can override this environment variable with the -e global option.

Alternatively, if the log message is short, you can specify it on the command line with the -m option, followed by the log message in quotes. See the example below.

New projects

If you are using CVS from the outset of your project, you will not have any existing code to import. The simplest way to start a project in this case is to create an empty directory, along with any needed subdirectories, and import that into CVS.

Example

Suppose you have some initial code for the "frobnitz" project in the directory src/frob under your home directory. Since your team hasn't really done any coding yet, this directory contains only a Makefile and README, and a docs subdirectory containing text files for the specifications and design.

# The following assumes CVSROOT is already set

~$ cd src/frob

 

~/src/frob$ cvs import -m "Initial import" frobnitz ourteam start

N frobnitz/Makefile

N frobnitz/README

cvs import: Importing /home/jrandom/cvs/frobnitz/docs

N frobnitz/docs/specs.txt

N frobnitz/docs/design.txt

 

No conflicts created by this import.

Now you have created a project "frobnitz" out of the contents of src/frob. However, src/frob is not associated with the CVS repository in any way. The next step is to check out a working copy.

Working directories

The code stored in the repository is in a special format called RCS, which stores not just the current version but also sufficient information to return to any previous version. As a result, the files in the repository should not be edited directly. Instead, you must check out a project or directory from the repository: that is, obtain a copy of a certain version of the code. Then you edit the copy, and at some point commit the code back into the repository.

In CVS, checking out a project puts the copy in what is called a working directory. The working directory contains a copy of (a certain version of) the code, along with CVS/ subdirectories that contain metadata: files describing exactly what code was checked out and where. CVS can use this information to keep track of changes you have made to the code since you checked it out; in general, you should not edit the metadata yourself.

cvs checkout

To create a working directory, use the cvs checkout subcommand (abbreviated cvs co). This command copies a certain project or directory from the repository into a subdirectory of the current directory.

cvs checkout usage

cvs [global-options] checkout [options] module

Check out a copy of the listed modules (a project or directory) from the repository. A working directory with the same name as the module will be created under the current directory, and a copy of the current version of the module (including any subdirectories) will be placed in that directory. If the working directory already exists, it will be updated instead.

Possible options include:

-d dir

Create the working directory with the name dir instead of the current directory.

-N

(Only useful with -d) Create the working directory as a subdirectory of dir instead of in that directory itself. For example,

~$ cvs co -N -d ~/src/ myproject

will create the working directory in ~/src/myproject/.

After creating your "frobnitz" project, it is time for you and your teammates to check out the code and begin working. Because the original ~/src/frob/ directory does not contain CVS metadata, it cannot be used as a working directory. Instead, you should check the code out into a new directory. After doing so (and verifying that it worked properly!), you should move your original directory somewhere out of the way, or even delete it; this ensures that you do not accidentally edit the code outside of CVS.

Checking out a project

# CVSROOT has been set already

~src/frob$ cd ..

~src$ cvs co frobnitz

cvs checkout: Updating frobnitz

U frobnitz/Makefile

U frobnitz/README

cvs checkout: Updating frobnitz/docs

U frobnitz/docs/specs.txt

U frobnitz/docs/design.txt

 

# Now you have a working directory in ~/src/frobnitz.

 

# Make sure it matches the original

~src$ diff -r frob frobnitz

Only in frobnitz: CVS

 

# The only difference is the new CVS directory, so you can delete the original.

~src$ rm -rf frob

 

# Change into the working directory and look around

~src$ cd frobnitz

~src/frobnitz$ ls

CVS/  docs/  Makefile  README

 

~src/frobnitz$ ls docs

CVS/  design.txt  specs.txt

 

~src/frobnitz$ ls CVS

Entries  Repository  Root

Keeping the working copy fresh: update

Now that you have a working directory, you can begin to add files and edit the existing files using your favorite text editor or IDE. Later, once you have completed a batch of changes, you can commit your changes to the repository so they will be available to others.

Suppose you want to add driver code to the project in the source file driver.cpp. First, create this file and write the code: perhaps a main function, for example. Then edit the README file to describe the purpose of the driver source file. You might even compile the driver program

While you are editing a working copy, it is a good idea to keep up-to-date with other people's changes to the code. You can do so with the cvs update subcommand. In addition to retrieving updates from the repository, this command displays a list of the files that differ:

~src/frobnitz$ cvs update

cvs update: Updating .

? driver

? driver.cpp

M README

cvs update: Updating docs

U design.txt

The character in the first column indicates how the file differs from the version in the repository. ? marks files, like your new driver.cpp and the driver executable, that CVS does not yet know about. M marks files like README that have been changed in the working copy. Files marked with U have been changed in the repository; the working copy is updated to match the version in the repository. Finally, files marked A (there are none in the above example) are new to the working copy, but will be added to the repository the next time you commit your changes.

From the example output above, it appears that someone else has changed docs/design.txt since we checked out the working directory. The cvs update command downloaded the new version of this file into the working copy; if you were to run update again, design.txt would no longer be listed, since it now matches the repository.

In general, it is a good idea to run cvs update frequently, perhaps before editing each file and before committing your changes. This will help ensure that you and your teammates stay in sync and remain aware of each others' changes. Furthermore, it makes it easy to identify and correct conflicting changes early, before they cause too many problems.

You should also be aware of the -d command option to cvs update. This option (which must come after the word update) tells CVS to also retrieve new subdirectories from the repository into the working copy. By default, only new and changed files in existing directories are updated.

Adding files and directories

In the previous section you created a new file called driver.cpp; however, CVS does not know about the file yet. To add the file to revision control, use the cvs add subcommand.

~src/frobnitz$ cvs add driver.cpp

cvs add: scheduling file `driver.cpp' for addition

cvs add: use 'cvs commit' to add this file permanently

 

~src/frobnitz$ cvs update

cvs update: Updating .

? driver

A driver.cpp

M README

cvs update: Updating docs

As the message suggests, cvs add does not yet add the code to the repository. Instead, it marks the file as "new" in the CVS/Entries file, so that it will be added at the next commit.

In general, you should add new source files, documentation, and control files such as Makefile to revision control with cvs add. However, since executables can easily be rebuilt by running make, there is usually little reason to add them to the repository.

Displaying changes in detail: cvs diff

As you saw earlier, cvs update can be used to show the list of files that you have modified in the working copy. Sometimes it is useful to have more detailed information about exactly what has changed. You can use the cvs diff subcommand to get a line-by-line list of all changes between your files and the versions they are based on. The list of changes can be viewed on-screen or redirected to a file; the output is in diff format, and can therefore be used with the patch utility. Lines beginning with > are new to the working copy, while those marked with < have been removed or replaced in the working copy.

cvs diff takes the same command-line options as the Unix diff command. There are also additional command-line options to compare different versions of the project, instead of just the working copy; see the CVS documentation for more information.

Merging and conflicts

When you run cvs update, you may find that someone else has been working on the very same files as you. For example, while you added driver.cpp and updated the README file, perhaps someone else added a new frob.cpp file and also updated README. When you perform the update, CVS will try to automatically merge your changes with the updates from the repository:

~src/frobnitz$ cvs update

cvs update: Updating .

? driver

RCS file: /home/jrandom/CVS/frobnitz/README,v

retrieving revision 1.1

retrieving revision 1.2

Merging differences between 1.1 and 1.2 into README

M README

cvs update: Updating docs

 

# If you look at README now, you will find that it still has your

# changes, as well as those from the repository.

Sometimes, however, CVS is not able to merge the changes. This typically happens when you and another developer have both changed the very same lines of code. In this case, CVS will report a conflict and require you to manually fix the problem. In cvs update, files with a conflict are marked with the letter C. You cannot commit your code until you fix the conflict somehow.

~src/frobnitz$ cvs update

cvs update: Updating .

? driver

RCS file: /home/jrandom/CVS/frobnitz/README,v

retrieving revision 1.1

retrieving revision 1.2

Merging differences between 1.1 and 1.2 into README

rcsmerge: warning: conflicts during merge

cvs update: conflicts found in README

C README

cvs update: Updating docs

 

# Try to save our changes to the repository.

~/src/frobnitz$ cvs commit

 

cvs commit: Examining .

cvs commit: file `README' had a conflict and has not been modified

cvs commit: Examining docs

cvs [commit aborted]: correct above errors first!

 

 

# It looks like we'll have to actually fix the conflict

When a conflict occurs, CVS inserts special markers into your file which show both versions of the code. The lines between the markers <<<<<<< and ======= are your version of the code, while those between ======= and >>>>>>> are the updated version from the repository. To fix the conflict, you must figure out how to combine the changes to get the effects of both; this sometimes will require the help of the other developer. Edit the file to manually merge the differences and remove the markers, then commit the changes. More information, with a detailed example, is available in section 10.3 of the CVS manual.

Committing your code to the repository

After some amount of editing and updating, you will be ready to check your modified code into the repository. This is accomplished with the cvs commit command (sometimes abbreviated cvs ci, for "check in").

If there are any out-of-date files in your working copy, cvs commit will give an error message ("Up-to-date check failed") and refuse to check in your changes. If this happens, simply run cvs update and commit again.

~/src/frobnitz$ cvs commit

cvs commit: Examining .

cvs commit: Examining docs

cvs commit: Up-to-date check failed for `design.txt'

cvs [commit aborted]: correct above errors first!

 

# Someone has made another change, need to update.

~/src/frobnitz$ cvs update

cvs update: Updating .

A driver.cpp

M README

cvs update: Updating docs

U design.txt

 

# Now we can commit.

~/src/frobnitz$ cvs ci

cvs commit: Examining .

cvs commit: Examining docs

  cvs now launches a text editor, where you can

  enter a log message.  When you are finished, save

  the file and quit, and the commit will proceed.

Checking in driver.cpp:

/home/jrandom/CVS/frobnitz/driver.cpp,v  <--  driver.cpp

initial revision: 1.1

done

Checking in README:

/home/jrandom/CVS/frobnitz/README,v  <--  README

new revision: 1.3; previous revision: 1.2

done

When you commit your code, CVS launches a text editor for you to add a log message. This message will become part of the version history of the changed files, allowing your teammates to understand how your code has evolved. In your log message, you should describe the content and purpose of the changes you have made to the code. If you have trouble remembering what changes you made, refresh your memory with cvs diff.

cvs commit usage

cvs [global-options] commit [options] [filenames...]

Commit the specified files or directories to the repository. If you do not specify any filenames, commits the entire working directory to the repository. Possible options include:

-m "message"

Use message as the log message instead of launching an editor.

-F logfile

Use the contents of logfile as the log message instead of launching an editor.

-l

Operate only on the specified files or the current directory, but not on any subdirectories.

Making effective use of revision control

  1. Write good log messages.

Your log mesages should be clear, descriptive, and easy to understand. The log messages should list the changes, additions, and deletions you made, including the names of affected classes, methods, and functions. Even more important, you should list the reason for each change. Related changes should be grouped together. Some people prefer to list the changes in the present tense ("Add new class..."), others in the past ("Added new class..."; your team should pick one convention and stick with it.

Log message

driver.cpp (main): Added a new driver program. This first version just prints a message and returns EXIT_SUCCESS.

README: Mentioned new driver.cpp file.

CVS: ----------------------------------------------------------------------
CVS: Enter Log.  Lines beginning with `CVS:' are removed automatically
CVS: 
CVS: Committing in .
CVS: 
CVS: Modified Files:
CVS:    README
CVS: Added Files:
CVS:    driver.cpp
CVS: ----------------------------------------------------------------------
  1. Commit frequently.

Instead of modifying your programs in huge batches of changes, it is usually a good idea to make a series of small, incremental changes.

    1. If there are problems with the changes, they can be identified and corrected earlier. If there has been a large batch of changes, it can be unclear which individual change caused a new bug. If you commit each change separately, you can look for the bug in each committed version to find discover it cause.
    2. It is easier to undo a single change while retaining others.
    3. Other developers can see what you are working on, and know that your are making contributions to the project.
    4. Conflicts can be spotted and corrected earlier—and if everyone updates frequently, too, they can become less common in the first place.
  1. Commit working code.

This imperative conflicts somewhat with 2., but it is just as important. If your team (or even one member) makes a habit of committing broken code, you will quickly learn that it is dangerous to run cvs update (it might break your code!), and it will become difficult to keep the team focused on a single target.

Of course, it is impossible to avoid bugs altogether, but you can take steps to make sure your changes don't stop work for everyone else. At a bare minimum, make sure your code compiles before checking it in. You will find that making small, incremental changes (2. above) helps this task, by reducing the likelyhood you will face pages and pages of syntax errors to correct.

Beyond just compiling your program, you should test it to make sure it doesn't contain any regressions: detrimental changes in behavior from the previous version. Automated unit testing is very helpful here, because then a simple "make test" will perform a number of tests to tell you whether the code still works. Of course, your team has to write the tests!

Viewing logs

Of course, the log messages collected by CVS would be useless if there were no way to view them. CVS provides the cvs log subcommand to view the history of a file, including log messages. If you do not specify a filename, the command shows the history of all files (including those that have not changed). In addition to the log messages, cvs log displays the date and committer of each revision, the number of lines added/deleted, the revision number, and a list of all tags and branches.

~/src/frobnitz$ cvs log README

RCS file: /home/jrandom/CVS/frobnitz/README,v

Working file: README

head: 1.3

branch:

locks: strict

access list:

symbolic names:

        start: 1.1.1.1

        ourteam: 1.1.1

keyword substitution: kv

total revisions: 4;     selected revisions: 4

description:

----------------------------

revision 1.3

date: 2007/08/17 16:11:33;  author: jrandom;  state: Exp;  lines: +2 -0

driver.cpp (main):  Added a new driver program.  This first version

just prints a message and returns EXIT_SUCCESS.

 

README: Mentioned new driver.cpp file.

----------------------------

revision 1.2

date: 2007/08/17 15:48:13;  author: jdoe;  state: Exp;  lines: +2 -0

frob.cpp: Added new file implementing class frob.

 

README: Described new frob.cpp file.

----------------------------

revision 1.1

date: 2007/08/13 19:04:43;  author: jrandom;  state: Exp;

branches:  1.1.1;

Initial revision

----------------------------

revision 1.1.1.1

date: 2007/08/13 19:04:43;  author: jrandom;  state: Exp;  lines: +0 -0

Initial import

Line-by-line history: cvs annotate

Sometimes it is helpful to get more precise information about the version history of the source code. For example, it is often useful to know who wrote or last modified a particular line of code, or what other code changed at the same time. CVS provides the cvs annotate subcommand for this purpose: it displays a file or files with annotations indicating the revision, date, and committer of the last change of each line. As with cvs log, this subcommand can be given a filename, or used alone to display annotations for every file in the project.

Working with revisions

You have seen how to use CVS to keep a team of developers in sync and make sure everyone is working with the latest code. Sometimes, however, it is necessary to work with different versions of the code. For example, if you discover a bug, you might want to look at, or even compile and run, older versions of the code to determine whether the bug was already around back then, or whether it is new.

In CVS, each file in the repository has a number of revisions. Each revision is a dotted sequence of numbers, such as 1.2 or 1.3.1.2. A revision such as the latter with more than two components is part of a branch.

Each time you modify and commit a file, the last component of the file's revision increases by one. For example, 1.2 becomes 1.3, and 1.3.1.2 becomes 1.3.1.3. If you commit an entire directory, only those files that have changed will get new revisions. Thus the files in a project will often have different revision numbers. The cvs log subcommand can be used to list the revisions of a file.

Many CVS subcommands allow you to work with versions other than the most recent. Among the subcommands you have seen, checkout, update, and diff all allow you to work with versions other than the latest.

The following are command options, and thus must follow the subcommand name.

-r revision

Use the specified revision instead of the latest. The revision may be either a number (most useful when dealing with a single file) or a symbolic tag.

-D date

Use the revision as of the specified date. This option selects the last revision that was made on or before the given date. There are a number of date formats allowed; if the date contains a space, you must surround it with quotes.

-D 2005-02-11

February 11, 2005

-D "2005-11-02 13:00"

1 pm on November 2, 2005

-D "12 Sep"

September 12 of this year

-D yesterday

What it sounds like

-D "last Friday"

You guessed it

-D "2 hours ago"

Likewise

Marking revisions: tag

It can be difficult to remember the revision numbers for particular versions of your source code. This is especially true when there are multiple source files, each with a different revision number. You can avoid this difficulty by creating a symbolic tag. A tag is a name which refers to a particular version of some set of files (perhaps a single file, or the whole repository, or anything in between). To create a tag, use the cvs tag subcommand:

cvs tag usage

cvs [global-options] tag [options] tagname [filenames...]

Add the current versions of the specified files to the symbolic tag tagname. If no files are specified, add the current versions of all files to the tag (this is the usual usage). For the purpose of adding tags, the "current version" of a file is the revision that is currently checked out, ignoring uncommitted changes. Possible options include:

-b

Make the tag a branch, allowing new versions to be derived from the tag without affecting mainline development.

-d

Delete the specified tag.

-F

If the tag already exists for a file, move the tag to the current version.

-r rev
-D date

Use the specified revision or date for the tag, instead of the current version.

When, at long last, your team has finished the implementation and testing, you are ready to release version 1.0. You know you might have to make some changes in the future, so you think it's probably a good idea to tag the current revision. That way, you can easily go back to version 1.0, for example to attempt to reproduce user bug reports.

# We finally finished the project, tag it as version 1.0

~/src/frobnitz$ cvs tag release-1.0

cvs tag: Tagging .

T Makefile

T README

T driver.cpp

T frob.cpp

T nitz.cpp

cvs tag: Tagging docs

T docs/specs.txt

T docs/design.txt

T docs/manual.txt

T docs/known-bugs.txt

cvs tag: Tagging tests

T tests/Makefile

T tests/test1.in

T tests/test1.out

T tests/test2.in

T tests/test2.out

 

~/src/frobnitz$ write jdoe

I just tagged version 1.0, let's have a party!

Once you have created a tag, you can use it wherever you could use a revision number. See the sections "Checking out a particular version" for examples.

Comparing two versions

The cvs diff command can show the differences between the working copy of a file and any version in the archive. For example, cvs diff -r 1.1 README will show all the changes to the file README since the very first version (1.1). You can also specify two different revisions or dates to show the differences between those versions (ignoring the working copy altogether). So cvs diff -r release-1.0 -D "last monday" shows the differences, in all files, between the revision tagged "release-1.0" and Monday's version.

Checking out a particular revision

Sometimes it is helpful to go back to a previous version of a project or file. For example, if you discover a bug in the program, it can be useful to know whether it was present in older versions of the program or it is new. You can check out a particular version, instead of the latest, by providing the -r or -D option to cvs checkout.

~/src$ cvs co -d frobnitz-old -D "2 weeks ago" frobnitz

cvs checkout: Updating frobnitz-old

U frobnitz-old/Makefile

U frobnitz-old/README

U frobnitz-old/driver.cpp

cvs checkout: Updating frobnitz-old/docs

U frobnitz-old/docs/specs.txt

U frobnitz-old/docs/design.txt

 

# Now the directory frobnitz-old contains the code from one week ago.

If you already have a working directory, you can change it to a particular revision by passing the -r or -D option to cvs update.

~/src/frobnitz-old$ cvs update "2 weeks ago" frobnitz

cvs update: Updating .

U frobnitz-old/README

cvs update: driver.cpp is no longer in the repository

cvs checkout: Updating docs

U frobnitz-old/docs/design.txt

 

# Now the current directory contains the code from two weeks ago.

Returning to the current version

If the checked-out revision of a file is not the latest, and the revision is not a branch, you cannot commit your changes:

~/src/frobnitz-old$ cvs ci

cvs commit: Examining .

cvs commit: cannot commit with sticky date for file `driver.cpp'

cvs commit: Examining docs

cvs [commit aborted]: correct above errors first!

In order to commit, you must either return to the current version of the repository, or create a branch. The latter process is described in the next section. To return the current version, use cvs update -A. Note that your changes will have to be merged into the current versions of the files.

Branches

As you saw in the previous example, you cannot normally commit to an old version of the repository. With the way we have been using CVS so far, the revisions in the repository form a chronological sequence; new revisions can only be added to the end of the sequence. Branches allow you to eliminate this restriction by creating a new sequence of revision numbers. If the branch was based on revision 1.2 of a particular file, the revisions of that file in the branch will look like 1.2.1.1, 1.2.1.2, and so on. It is then possible to commit either to the branch, giving revision 1.2.1.3; or to the trunk, or default branch (sometimes called mainline), giving revision 1.3.

Why might you want a branch? Suppose you are working on an existing software project; perhaps it has already been released, and developers are working on version 2.0 in the CVS repository. In the meanwhile, one of your users discovers a serious bug in the release version, perhaps a security hole. Users want the bug fixed, so their information will be safe, but version 2.0 is nowhere near ready. What is a developer to do?

With branches, the answer is simple. Back when you released the software, you created a tag in the repository, named release-1.0. Now that you need to make changes to the release version, you can simply turn this tag into a branch, fix the bug, and commit the change to the branch. You then have a new revision of the code that has the bug fix, but is otherwise the same as the 1.0 release. You can now compile this, test it, and ship it to your users as version 1.1 or something similar. Furthermore, you can even merge the changes back into the trunk, to make sure that the bug is also fixed in version 2.0.

Branching is a fairly complicated topic, and we can touch on it only briefly here. For more information, see chapter 5 of the CVS manual online.

Creating a branch

To create a branch, you use cvs tag with the -b option. This creates a branch based on the revisions that were last checked out, ignoring changes in the working copy. You will almost always refer to branches by symbolic names, not revision numbers. You can use the -r or -D option to start the branch from a different version from the current one.

# Create a branch based on the release-1.0 tag, containing all files

~/src/frobnitz$ cvs tag -b -r release-1.0 bugfixes-1.0

cvs tag: Tagging .

T Makefile

T README

T driver.cpp

T frob.cpp

T nitz.cpp

cvs tag: Tagging docs

T docs/specs.txt

T docs/design.txt

T docs/manual.txt

T docs/known-bugs.txt

cvs tag: Tagging tests

T tests/Makefile

T tests/test1.in

T tests/test1.out

T tests/test2.in

T tests/test2.out

Using a branch

Note that when you create a new branch with cvs tag -b, CVS does not automatically switch your working copy over to that branch; if you commit new code, it will by default still go to the trunk. If you want to work in the branch, you must first use the same cvs checkout -r or cvs update -r command that you saw previously.

# Switch to the bugfixes-1.0 branch.

~/src/frobnitz$ cvs update -r bugfixes-1.0

cvs checkout: Updating .

U Makefile

U README

U driver.cpp

U frob.cpp

U nitz.cpp

cvs checkout: Updating docs

U docs/manual.txt

 

# Try to reproduce, isolate, and fix the bug, and test the resulting code.

# When you are finished, it is time to commit

 

~/src/frobnitz$ cvs ci

cvs commit: Examining .

cvs commit: Examining docs

cvs commit: Examining tests

  Enter a log message

Checking in driver.cpp:

/home/jrandom/CVS/frobnitz/driver.cpp,v  <--  driver.cpp

new revision: 1.5.1.2; previous revision: 1.5.1.1

done

RCS file: /home/jrandom/CVS/frobnitz/doc/Attic/changes.txt,v

Checking in changes.txt:

/home/jrandom/CVS/frobnitz/doc/Attic/changes.txt,v  <--  changes.txt

new revision: 1.1.2.1; previous revision: 1.1

done

 

# I'm finished working in the branch for now; switch back to the trunk:

~/src/frobnitz$ cvs update -A

cvs checkout: Updating .

U Makefile

U README

U driver.cpp

U frob.cpp

U nitz.cpp

cvs checkout: Updating docs

cvs update: changes.txt is no longer in the repository

U docs/manual.txt

You may have noticed that some of the files are created in a directory called Attic. This is the directory CVS uses internally to store files that aren't present in the latest revision (for example, see how cvs update -A removed changes.txt, since it only exists in the bugfixes-1.0 branch). In addition to files added in a branch, files that have been deleted and removed with cvs remove are also stored in the attic.

Merging branches

Though creating and working with branches is useful, it would be very annoying if you had to re-write all the changes in the trunk as well. CVS lets you save type by merging changes in the branch back into the repository. This is done with cvs update -j branch; this command finds all the changes that were made in the branch, and merges them into the current version. As with the automatic merging you have seen before, it is possible that conflicts will prevent the changes from being applied cleanly. In that case you must resolve the conflicts manually before you commit.

When you use cvs update -j, three versions of the code are relevant: the current code in the working copy, the first revision in the branch, and the last revision in the branch. The process is therefore often called three-way merging. You can think of the process algebraically: if W is the working copy, B the beginning of the branch, and E the end of the branch, three-way merge is kind of like the operation W = W + (E - B).

It is furthermore possible to merge only some of the changes in a branch, by specifying -j twice. In this case, the first -j is the first revision to consider (B in the formula above), and the second is the last revision to consider (E in the formula above).

Merging branches is a rather complicated topic, and there are a few caveats to keep in mind. If you are planning on merging branches in your project, it behooves you to read Chapter 5 of the CVS manual (and probably Chapter 4 as well).

Front-ends to CVS

TortoiseCVS

TortoiseCVS is a Windows program that integrates CVS features into Windows Explorer (the Windows file browser). Files that belong to a CVS working directory have small overlay icons showing their current status (out-of-date, up-to-date, changed in working copy, etc.); by right-clicking on a file or working directory, you can update, commit, view differences, and execute many other CVS commands.

TortoiseCVS is free software: not only is the software available as a free download, but you can also get the source code and make changes yourself. Its homepage is http://www.tortoisecvs.org/.

Eclipse

The Eclipse IDE has built-in support for CVS. In Eclipse, the Repositories view allows you to select CVS repositories (both local and remote), and check out projects from those repositories. If a project was checked out of a repository, you can right-click on the project or on files in the project to access a number of CVS operations: update, commit, diff, merge, revert to a previous version, and so on.

For more information on using CVS within Eclipse, see the following sections of the Eclipse documentation. You can access the documentation online, or from the Help | Help Contents... menu in Eclipse.

Netbeans

Other version-control systems

CVS has, since its introduction in 1986, become the most widely-supported version control system, especially on Unix systems. Some people argue, however, that it is beginning to show its age. In recent years a number of next-generation revision control systems have appeared to address perceived flaws in CVS.

Subversion

Of the next-generation revision control systems, Subversion is the closest to CVS in terms of features and usage. Indeed, many of the subcommands of the svn command-line interface have the same name and syntax as their CVS counterparts. There are, however, a number of improvements, such as better support for copying and renaming files, a separate command for easier three-way merging, more efficient handling of large files and binary files (such as graphics), and many more.

Subversion is available on Multilab and SWEB, but not (currently) Multilab. The main web site is at http://subversion.tigris.org/; you can download source code and binaries (including a Windows command-line program) from this site. Also useful is the book Version Control with Subversion, available for free online, and also in paper form. You can also obtain documentation with the svn help subcommand. Finally, TortoiseSVN integrates Subversion into Windows Explorer, much like TortoiseCVS does for CVS. TortoiseSVN is free software, and available for download.

Visual SourceSafe

Visual SourceSafe is a proprietary revision control system by Microsoft that integrates into Visual Studio. More information is available from Microsoft's Visual Studio Developer Center.

Distributed revision control

Even more recently, a new model of revision control has appeared, called distributed revision control. In these new systems, there is no central repository for the code. Instead, each developer's working directory contains history information for that user's changes—like a local repository. To share code, developers exchange "change sets" or "patch sets" amongst one another, using operations such pull (download someone else's patch set) and push (send someone your patch set). Using these patch sets, a developer can merge others' changes into his or her local repository; thus developers can stay up-to-date while working more independently. Distributed revision control is becoming popular for open-source projects such as the Linux kernel.

Notes

  1. Fred Brooks, The Mythical Man-Month. Addison-Wesley, 1995. ISBN 200-201-83595-9.