CVS Tutorial

Last updated $Date: 2006-12-01 09:00:05 $

Martti Kuparinen <martti.kuparinen@iki.fi>

Abstract

This small tutorial describes how CVS can be used within a project to maintain version history. Two real-life examples are shown in addition to basic CVS operations.


See also: the questions and their answers.


Table of Contents

1. Introduction
2. Preparations
3. Getting a working copy
4. Updating the working copy
5. Adding files and directories
6. Removing files and directories
7. Making the modifications visible to others
8. Viewing differences
9. Viewing logs
10. Setting and viewing tags
11. Making branches
12. Merging changes from branches
13. Third-party sources and local modifications
14. Taking backup of the repository
15. Example: Creating a new project from scratch
16. Example: Project with third-party sources
17. Example: CVS over SSH tunnel
18. References

1. Introduction

CVS is version control system used to record the history of source files. This small tutorial gives two real-life examples as well some basic operations how CVS can be used to have a better control of the project files.


2. Preparations

This tutorial assumes that remote CVS repositories are accessed using a secure SSH tunnel. Therefore it's important to set CVS_RSH variable in the shell's initialization scripts. Without the following setting the (unsecure) rsh will be used to access remote repositories.

# echo "export CVS_RSH=ssh" >> ~/.bashrc

Sometimes CVS makes too much noise about itself. The author always adds this to the CVS configuration file:

echo "cvs -q" >> ~/.cvsrc

3. Getting a working copy

Each user must first get his/her own working copy of the project files before any changes can be made. The working copy is fetched with the cvs co command and it needs to be executed only once.

# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot co -P src

4. Updating the working copy

The cvs update command can be used to update the working copies. The following command checks for new directories (-d), removed empty directories (-P) and gets rid of so-called sticky tags (-A). It is also possible to get a certain version by using the -r ver option.

# cvs update -dPA

or 

# cvs update -dPA -r name-of-the-branch

5. Adding files and directories

New files can be added to the repository by running cvs add after creating the file. Please note that the new file is not visible to other users before cvs ci is executed.

# vi filename
# cvs add filename
# cvs ci

New directories are added like files but there is no need to run cvs ci.

# mkdir directory
# cvs add directory

6. Removing files and directories

Files can be removed from the repository by running cvs rm after manually removing the file first from the local filesystem. Please note that the file is not actually removed from the repository, but marked as removed. This means that it's still possible to retrieve old versions of the file later if needed.

# rm filename
# cvs remove filename
# cvs ci -m "This file is no longer used" filename

It is not possible to remove directories (except by removing them directly from the repository which is a BAD thing to do) but it's possible to hide empty directories with the following command.

# cvs update -P

7. Making the modifications visible to others

Use the cvs ci command to make the local modifications visible to other users. Before checking in the changes:

The local changes can be checked in with

# cvs ci [-m "optional message"] [filename]
# cvs update -dPA

8. Viewing differences

The cvs diff command can be used to see differences between different versions.

[Show local changes against the checked-out version]
# cvs diff -u [filename]

[Show changes between two versions]
# cvs diff -u -r 1.42 -r 1.43 filename
# cvs diff -u -r BEFORE-XYZ -r HEAD [filename]

9. Viewing logs

The cvs log command can be used to see the commit logs. Please remember to use meaningful commit messages as "fixed bugs" is not very useful.

# cvs log filename | less

10. Setting and viewing tags

The cvs tag and cvs rtag commands can be used to set and remove tags. Tag is a symbolic name which identifies a certain revision of a file. Tag names must start with a letter and can contain letters, digits, '-' and '_'. So, "release 1.2" could be tagged with the name RELEASE-1_2.

# cd ~/myproj/src
# cvs tag RELEASE-1_2

Later (when e.g. Foo 1.3 is already out and in use) it is easy to get the sources of Foo 1.2 by either checking out a fresh working copy or updating the current working copy with the -r flag.

# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot \
  co -P -r RELEASE-1_2 src/foo

or

# cd src/foo
# cvs update -dPA -r RELEASE-1_2

Existing tags can be viewed by running the cvs status -v command.

# cvs status -v filename

11. Making branches

Foo 1.3 is already out and in use, but there is a need to create a patch for the good old Foo 1.2. The solution is to create a branch from the RELEASE-1_2 tag.

# cvs rtag -b -r RELEASE-1_2 RELEASE-1_2_x src/foo
# cvs checkout -r RELEASE-1_2_x src/foo
# cd src/foo
[Hack the sources]
# cvs ci -m "Fixed nasty memory allocation bug"

The changes commited will only affect he newly created branch, i.e. the -r flag is said to be sticky. Sticky tags can be removed (i.e. return to the HEAD branch) with cvs update -A.

When creating a branch it is important to set a tag for the branch point and then create the branch from that tag. In the previous example we already had a tag (RELEASE-1_2) which was set when version 1.2 was released. The following example shows how to first create a tag for the branch point and then the branch itself.

[Tag the branch point]
# cvs tag TEST-BP

[Create the branch]
# cvs tag -b -r TEST-BP TEST
# cvs update -r TEST
# cvs ci -m "Fixed loop problem in a more efficient way"

Other users can work on the same experimental code by checking out the that branch.

# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot \
  co -P -r TEST src/foo

or

# cd src/foo
# cvs update -dPA -r TEST

12. Merging changes from branches

Now let's consider the following revision tree:

    +-----+   +-----+   +-----+   +-----+
    | 1.1 |---| 1.2 |---| 1.3 |---| 1.4 |       <== The HEAD branch
    +-----+   +-----+   +-----+   +-----+
                 |
                 |   +---------+   +---------+
                 +---| 1.2.2.1 |---| 1.2.2.2 |  <== The TEST branch
                     +---------+   +---------+

Version 1.2 was given a TEST-BP tag ("test branch point") and the TEST branch was created from that tag. The tag TEST always refers to the latest version within that branch (in this case revision 1.2.2.2).

Next we merge changes made in the TEST branch to the HEAD branch with the cvs update -j command.

[Get the HEAD branch]
# cvs update -dPA

[Merge the changes]
# cvs update -d -kk -j TEST-BP -j TEST              # Merge
[Fix possible conflicts]
# cvs ci -m "Take XYZ from the TEST branch"         # Check in changes

[Get the TEST branch and tag it]
# cvs update -dPA -r TEST
# cvs tag TEST-MERGED-TO-HEAD                       # Put a new tag

Now let's assume the development continues on the TEST branch and we need to merge again. This time we should merge from 1.2.2.2 (which was given the tag TEST-MERGED-TO-HEAD) to the latest version within the TEST branch.

[Get the HEAD branch]
# cvs update -dPA

[Merge the changes]
# cvs update -d -kk -j TEST-MERGED-TO-HEAD  -j TEST # Merge
[Fix possible conflicts]
# cvs ci -m "Take XYZ from the TEST branch"

[Get the TEST branch and tag it again]
# cvs update -dPA -r TEST
# cvs tag -d TEST-MERGED-TO-HEAD                    # Remove the tag
# cvs tag TEST-MERGED-TO-HEAD                       # Tag it again

It's important to understand that the merge is done between two revisions and each revision can be merged only once (see the TEST-MERGED-TO-HEAD in the previous examples). Trying to merge a revision more than once will result a conflict.


13. Third-party sources and local modifications

Let's assume a new project is started based on some existing code. The code used in this project is Foo version 1.0. Before making any modifications we import Foo 1.0 into the FOO vendor branch with the tag name FOO-1_0. After this the project members can execute cvs update -d to update their working copies (and get the previously imported Foo 1.0 in src/foo) and start modifying the original Foo 1.0 sources.

# cd /tmp
# tar xzf foo-1.0.tar.gz
# cd foo-1.0
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot import \
      -I ! -I CVS -ko -m "Imported Foo 1.0" src/foo FOO FOO-1_0

# cd ~/src
# cvs update -dPA

Later when Foo 1.1 is released we can import the new release into the FOO vendor branch with the tag name FOO-1_1 and merge changes made between versions 1.0 and 1.1 into the HEAD branch.

# cd /tmp
# tar xzf foo-1.1.tar.gz
# cd foo-1.1
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot import \
      -I ! -I CVS -ko -m "Imported Foo 1.1" src/foo FOO FOO-1_1

# cd ~/src/foo
# cvs update -d -kk -j FOO-1_0 -j FOO-1_1
# cvs update | grep ^C
[Fix possible conflicts]
# cvs ci -m "Upgraded Foo to version 1.1"
# cvs update -dPA

When the other project members now update their working copies they will get the new Foo 1.1 in src/foo with the previously made changes). In other words, src/foo is Foo 1.1 with all the local modifications made by the project members.


14. Taking backup of the repository

Generally it's a good idea to take a backup of the repository from time to time and before any big imports.

# ssh mylogin@host.name.com
# cd /home/projects/myproj
# tar czf 20051006.tar.gz cvsroot

If the import fails for some reason it's simple to move the corrupted repository away and extract the original version from the backup.

# ssh mylogin@host.name.com
# cd /home/projects/myproj
# mv cvsroot cvsroot.fail
# tar xzf 20051006.tar.gz

15. Example: Creating a new project from scratch

In this example a new project is started from scratch.

The project administrator must first create the CVS repository. In this example all project files will be stored in /home/projects/myproj and only the project members can access that directory. The project members must be in the grpname group. Please note how the "set-group-id" bit is set with the chmod command.

[Done on the CVS server]
# mkdir /home/projects/myproj
# mkdir /home/projects/myproj/cvsroot
# chgrp -R grpname /home/projects/myproj            # change group
# chmod 2770 /home/projects/myproj                  # make group writable
# cvs -d /home/projects/myproj/cvsroot init         # create the repository

or

[Done remotely]
# ssh mylogin@host.name.com mkdir -p /home/projects/myproj/cvsroot
# ssh mylogin@host.name.com chgrp -R grpname /home/projects/myproj
# ssh mylogin@host.name.com chmod 2770 /home/projects/myproj
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot init

Next we need an empty directory which will be imported as a new "project". The project is called src in the following example. Please note that foo and bar are required but not really used as the imported directory is empty.

# mkdir /tmp/src
# cd /tmp/src
# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot \
  import -m "" src foo bar

The project members can now start to add new files and make changes to files created by other members. New files can be added with cvs add filename, existing files modified and unwanted files removed with cvs rm -f filename. Modifications to the source tree should be reviewed with cvs diff -u before checking in the changes with cvs ci.

# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot co src
# cd src
# vi extension.c
# cvs add extension.c                               # add a new file
# vi existingfile.c                                 # modify an existing file
# cvs rm -f TODO                                    # remove an existing file
# cvs update -dPA                                   # what files were changed?
# cvs diff -u | less                                # see the modifications
# cvs ci -m "All TODOs implemented" TODO            # remove the TODO file
# cvs ci -m "Added support for XYZ"                 # check in other files
# cvs update -dPA                                   # update all files

16. Example: Project with third-party sources

In this example a new project is started based on some existing code. The code used in this project is Foo version 1.0.

The project administrator must first create the CVS repository. In this example all project files will be stored in /home/projects/myproj and only the project members can access that directory. The project members must be in the grpname group. Please note how the "set-group-id" bit is set with the chmod command.

[Done on the CVS server]
# mkdir -p /home/projects/myproj/cvsroot
# chgrp -R grpname /home/projects/myproj            # change group
# chmod 2770 /home/projects/myproj                  # group writable
# cvs -d /home/projects/myproj/cvsroot init         # create the repository

or

[Done remotely]
# ssh mylogin@host.name.com mkdir -p /home/projects/myproj/cvsroot
# ssh mylogin@host.name.com chgrp -R grpname /home/projects/myproj
# ssh mylogin@host.name.com chmod 2770 /home/projects/myproj
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot init

After creating an empty repository the project administrator must import the original Foo 1.0 sources into the FOO vendor branch. These source files will be located in the src/foo directory.

# cd /tmp
# tar xzf foo-1.0.tar.gz                            # extract the original files
# cd foo-1.0
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot import \
  -I ! -I CVS -ko -m "Imported Foo 1.0" src/foo FOO FOO-1_0

The project members can now check out their own working copies and start making modifications and improvements. New files can be added with cvs add filename, existing files modified and unwanted files removed with cvs rm -f filename. Modifications to the source tree should be reviewed with cvs diff -u before checking in the changes with cvs ci.

# cvs -d johndoe@host.name.com:/home/projects/myproj/cvsroot co -P src
# cd src
# vi extension.c
# cvs add extension.c                               # add a new file
# vi existingfile.c                                 # modify an existing file
# cvs rm -f config.log                              # remove an existing file
# cvs update -dPA                                   # what files were changed?
# cvs diff -u | less                                # see the modifications
# cvs ci -m "Removed configuration log" config.log  # remove the log file
# cvs ci -m "Added support for XYZ"                 # check in other files
# cvs update -dPA                                   # update all files

Later when Foo 1.1 is released the project administrator can import the new release into the repository and merge changes made between versions 1.0 and 1.1 into the main developement branch (also known as HEAD).

[Import the new release]
# cd /tmp
# tar xzf foo-1.1.tar.gz                            # extract the new files
# cd foo-1.1
# cvs -d mylogin@host.name.com:/home/projects/myproj/cvsroot import \
  -I ! -I CVS -ko -m "Imported Foo 1.1" src/foo FOO FOO-1_1

[Merge changes between official 1.0 and 1.1 releases into HEAD]
# cd ~/src/foo
# cvs update -d -kk -j FOO-1_0 -j FOO-1_1           # merge between 1.0 and 1.1
# cvs update | grep ^C                              # any conflicts?
[Fix possible conflicts]
# cvs ci -m "Upgraded Foo to version 1.1"           # check in changes
# cvs update -dPA                                   # update all files

Next time the project members update their working copies they will get the new Foo 1.1 sources.


17. Example: CVS over SSH tunnel

Sometimes the CVS repository is located on an internal network which is only reachable when using SSH tunnels. The next picture shows an example where SSH tunnels are required to access the internal CVS server. Access to inside.corporation.com is only allowed from outside.corporation.com, direct connections to inside.corporation.com from the Internet are rejected by the firewall.

Network

In order to access inside.corporation.com, we must first connect to outside.corporation.com with SSH. This can be done with the following cvstunnel script.

#!/bin/sh
ssh -L10022:inside.corporation.com:22 johndoe@outside.corporation.com

Next we need to create SSH host alias so that we will never see those "host key changed" errors if making SSH connections to the same computer's SSH server (the SSH server running at localhost:22). Add these lines to your ~/.ssh/config file.

Host inside_corporation_com
  HostName localhost
  Port 10022
  UserKnownHostsFile /home/johndoe/.ssh/inside_corporation_com

Now we need to run the newly create cvstunnel script on a terminal in order to connect to outside.corporation.com and open a local port (10022) which will be forwarded to inside.corporation.com. Now on another terminal we can access the CVS repository by using the SSH hostname alias instead of the real hostname.

## On terminal 1
# cvstunnel

## On terminal 2
# cvs -d johndoe@inside_corporation_com:/home/projects/myproj/cvsroot co -P src
# cd src
[ Do some work ]
# cvs update -dPA

18. References