Last updated $Date: 2006-12-01 09:00:05 $
Martti Kuparinen <martti.kuparinen@iki.fi>
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.
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.
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
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
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
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
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
Use the cvs ci
command to make the local modifications visible
to other users. Before checking in the changes:
cvs update -dPA
to update the local working copy and fix
possible conflicts by opening files with conflicts with a text editorcvs diff -u
and
make sure the changes look reasonableThe local changes can be checked in with
# cvs ci [-m "optional message"] [filename] # cvs update -dPA
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]
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
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
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
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.
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.
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
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
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.
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.
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