This document originally from http://www.cvshome.org/docs/blandy.html
Thu Dec 12 11:30:12 EST 2002

Introduction to CVS

by Jim Blandy

Modified for Roselab to maintain LINUS by Pat Fleming

The snippets of actual code below are not from LINUS but from the original examples. However, the filenames and paths are correct for using CVS to edit LINUS files on grserv.

What is CVS for?

CVS maintains a history of a source tree, in terms of a series of changes. It stamps each change with the time it was made and the user name of the person who made it. Usually, the person provides a bit of text describing why they made the change as well. Given that information, CVS can help developers answer questions like:

  • Who made a given change?
  • When did they make it?
  • Why did they make it?
  • What other changes did they make at the same time?

How to use CVS -- First Sketch

Before discussing too many vague terms and concepts, let's look over the essential CVS commands.

Setting your repository

CVS records everyone's changes to a given project in a directory tree called a repository . CVS will remember where this is after the first time you use CVS if you keep your local working directory tree and just update it (see below). If you remove your local working directory tree you must give the command below with full path to copy the repository tree to your local working directory tree again.

On our system, the CVS repository is '/opt/cvsroot' on grserv. To use CVS on other machines in the Roselab you will want to enter the following commands:


setenv CVS_RSH ssh
setenv CVSROOT  user@grserv.bph.jhu.edu:/opt/cvsroot

where "user" is your login name.

Better yet put those commands in your .tcshrc or .cshrc file.

Or if you are using the bash shell put the following commands in your .bashrc file:


export  CVS_RSH= "ssh"
export CVSROOT=  "user@grserv.bph.jhu.edu:/opt/cvsroot"   
       
Or a third option is just to enter the relevant information on the command line as shown in the next example.

Checking out a working directory

CVS doesn't work on ordinary directory trees; you need to work within a directory that CVS created for you. Just as you check out a book from a library before taking it home to read it, you use the cvs checkout command to get a directory tree from CVS before working on it. For example, suppose you are working on a project named PYLINUS, and it is in the linus subdirectory in the CVSROOT directory:

(In the following "user" is your login name.)


$ cd dir/with/no/linus/

$ cvs -d user@grserv.bph.jhu.edu:/opt/cvsroot checkout linus/PYLINUS
cvs checkout: Updating linus/PYLINUS
U linus/PYLINUS/install_data.py
U linus/PYLINUS/setup.cfg
U linus/PYLINUS/setup.py
cvs checkout: Updating linus/PYLINUS/Src
U linus/PYLINUS/Src/Makefile
.
.
.
U linus/PYLINUS/Src/linusc.c
U linus/PYLINUS/Src/sedscript
cvs checkout: Updating linus/PYLINUS/pylinus
U linus/PYLINUS/pylinus/Atom3d.py
U linus/PYLINUS/pylinus/__init__.py
U linus/PYLINUS/pylinus/add_waters.py
U linus/PYLINUS/pylinus/bumpcheck.py
.
.
.
cvs checkout: Updating linus/PYLINUS/pylinus/utils
U linus/PYLINUS/pylinus/utils/Rasmol.py
U linus/PYLINUS/pylinus/utils/__init__.py
U linus/PYLINUS/pylinus/utils/concnt.py
.
.
.
$ 


The command cvs checkout linus/PYLINUS means, "Check out the source tree called PYLINUS from the repository specified by the CVSROOT environment variable."

(If you don't specify linus/PYLINUS, but just checkout linus you will get the complete distribution of LINUS including

BIOMOL/     LINUS_2_0/  LINUS_2_2/  LINUS_2_4/  manual/     PJFLINUS/
LINUS_1_0/  LINUS_2_1/  LINUS_2_3/  LINUS_2_5/  manual_23/  PYLINUS/
You can specify a linus distribution such as,

cvs checkout linus/LINUS_1_0

to obtain only the 1.0 version.)

CVS puts the tree in a subdirectory named 'linus/PYLINUS':


$ cd linus/PYLINUS

$ ls -l
total 28
drwxrwxrwx    2 fleming  users        4096 Dec 13 14:50 CVS/
-rw-rw-rw-    1 fleming  users        7611 Dec 13 14:23 install_data.py
drwxrwxrwx    4 fleming  users        4096 Dec 13 14:50 pylinus/
-rw-rw-rw-    1 fleming  users          37 Dec 13 14:23 setup.cfg
-rw-rw-rw-    1 fleming  users         924 Dec 13 14:23 setup.py
drwxrwxrwx    3 fleming  users        4096 Dec 13 14:50 Src/


Your working copies of the linus sources are in directories pylinus and Src. However, the subdirectory called `CVS' (at the top) is different. CVS uses it to record extra information about each of the files in that directory, to help it determine what changes you've made since you checked it out.

Making changes to files

Once CVS has created a working directory tree, you can edit, compile and test the files it contains in the usual way -- they're just files.

For example, suppose we try compiling the package we just checked out: (This will overwrite your current working installation of pylinus in /usr/local/python[version]/site-packages/pylinus/, so make a backup of that if you are not regularly using the CVS version).


$ python setup.py install
running install
running build
running build_py
creating build
creating build/lib.darwin-6.2-PowerMacintosh-2.2
creating build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/__init__.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/add_waters.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/Atom3d.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
.
.	(many lines deleted)
.
byte-compiling /usr/local/lib/python2.2/site-packages/pylinus/utils/torsion.py to torsion.pyo
byte-compiling /usr/local/lib/python2.2/site-packages/pylinus/utils/viewout.py to viewout.pyo
removing /tmp/@5680.0.py
running install_data
copying pylinus/utils/ribosome.dat -> /usr/local/lib/python2.2/site-packages/pylinus/utils
$


This procedure will also compile and install the C coded subroutines that LINUS uses if available.

Merging your changes

Since each developer uses their own working directory, the changes you make to your working directory aren't automatically visible to the other developers on your team. CVS doesn't publish your changes until you're ready. When you're done testing your changes, you must commit them to the repository to make them available to the rest of the group. We'll describe the cvs commit command below.

However, what if another developer has changed the same files you have, or the same lines? Whose changes should prevail? It's generally impossible to answer this question automatically; CVS certainly isn't competent to make that judgment.

Thus, before you can commit your changes, CVS requires your sources to be in sync with any changes committed by the other team members. The cvs update command takes care of this:


$ cvs update

cvs update: Updating .

U Makefile

RCS file: /opt/cvsroot/linus/PYLINUS/Src/linusc.c,v

retrieving revision 1.6

retrieving revision 1.7

Merging differences between 1.6 and 1.7 into linusc.c

M linusc.c
$ 


Let's look at this line-by-line:

U Makefile
A line of the form `U file' means that file was simply Updated; someone else had made a change to the file, and CVS copied the modified file into your home directory.
RCS file: ...
retrieving revision 1.6
retrieving revision 1.7
Merging differences between 1.6 and 1.7 into linusc.c
These messages indicate that someone else has changed `linusc.c'; CVS merged their changes with yours, and did not find any textual conflicts. The numbers `1.6' and 1.7 are revision numbers, used to identify a specific point in a file's history. Note that CVS merges changes into your working copy only; the repository and the other developers' working directories are left undisturbed. It is up to you to test the merged text, and make sure it's valid.
M linusc.c
A line of the form `M file' means that file has been Modified by you, and contains changes that are not yet visible to the other developers. These are changes you need to commit. In this case, `linusc.c' now contains both your modifications and those of the other user.

Since CVS has merged someone else's changes into your source, it's best to make sure things still work:


$ python setup.py install
running install
running build
running build_py
creating build
creating build/lib.darwin-6.2-PowerMacintosh-2.2
creating build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/__init__.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/add_waters.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
copying ./pylinus/Atom3d.py -> build/lib.darwin-6.2-PowerMacintosh-2.2/pylinus
.
.       (many lines deleted)
.
$ 


It seems to still work.

Committing your changes

Now that you have brought your sources up to date with the rest of the group and tested them, you are ready to commit your changes to the repository and make them visible to the rest of the group. The only file you've modified is `linusc.c', but it's always safe to run cvs update to get a list of the modified files from CVS:


$ cvs update

cvs update: Updating .

M linusc.c

$


As expected, the only file CVS mentions is 'linusc.c'; it says it contains changes which you have not yet committed. You can commit them like so:


$ cvs commit linusc.c


At this point, CVS will start up your favorite editor and prompt you for a log message describing the change. When you exit the editor, CVS will commit your change:


Checking in linusc.c;

/opt/cvsroot/linus/PYLINUS/Src/linusc.c,v  <--  linusc.c

new revision: 1.8; previous revision: 1.7

$


Now that you have committed your changes, they are visible to the rest of the group. When another developer runs cvs update, CVS will merge your changes to `linusc.c' into their working directory.

Examining changes

At this point, you might well be curious what changes the other developer made to `linusc.c'. To look at the log entries for a given file, you can use the cvs log command:


$ cvs log linusc.c

 

RCS file: /opt/cvsroot/linus/PYLINUS/Src/linusc.c,v

Working file: linusc.c

head: 1.8

branch:

locks: strict

access list:

symbolic names:

keyword substitution: kv

total revisions: 8;     selected revisions: 8

description:

The one and only source file for LINUS

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

revision 1.8

date: 1996/10/31 20:11:14;  author: jimb;  state: Exp;  lines: +1 -1

(tcp_connection): Cast address structure when calling connect.

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

revision 1.7

date: 1996/10/31 19:18:45;  author: fred;  state: Exp;  lines: +6 -2

(match_header): Make this test case-insensitive.

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

revision 1.6

date: 1996/10/31 19:15:23;  author: jimb;  state: Exp;  lines: +2 -6

...

$ 


Most of the text here you can ignore; the portion to look at carefully is the series of log entries after the first line of hyphens. The log entries appear in reverse chronological order, under the assumption that more recent changes are usually more interesting. Each entry describes one change to the file, and may be parsed as follows:

`revision 1.8'
Each version of a file has a unique revision number. Revision numbers look like `1.1', `1.2', `1.3.2.2' or even `1.3.2.2.4.5'. By default revision 1.1 is the first revision of a file. Each successive revision is given a new number by increasing the rightmost number by one.
`date: 1996/10/31 20:11:14; author: jimb; ...'
This line gives the date of the change, and the username of the person who committed it; the remainder of the line is not very interesting.
`(tcp_connection): Cast ...'
This (pretty obviously) is the log entry describing that change.

The cvs log command can select log entries by date range, or by revision number; see the manual for details on this.

If you would actually like to see the change in question, you can use the cvs diff command. For example, if you would like to see the changes Fred committed as revision 1.7, you can use the following command:


$ cvs diff -c -r 1.6 -r 1.7 linusc.c


Before we look at the output from this command, let's look at what the various parts mean:

-c
This requests that cvs diff use a more human-readable format for its output. (I'm not sure why it isn't the default.)
-r 1.6 -r 1.7
This tells CVS to display the changes needed to turn revision 1.6 of linusc.c into revision 1.7. You can request a wider range of revisions if you like; for example, -r 1.6 -r 1.8 would display both fred's changes and your most recent change. (You can even request that changes be displayed backwards -- as if they were being undone -- by specifying the revisions backwards: -r 1.7 -r 1.6. This sounds odd, but it is useful sometimes.)
linusc.c
This is the name of the file to inspect. If you don't give it specific files to report on, CVS will produce a report for the entire directory.

Here is the output from the command:


Index: linusc.c

===================================================================

RCS file: /opt/cvsroot/linus/PYLINUS/Src/linusc/linusc.c,v

retrieving revision 1.6

retrieving revision 1.7

diff -c -r1.6 -r1.7

*** linusc.c     1996/10/31 19:15:23     1.6

--- linusc.c     1996/10/31 19:18:45     1.7

***************

*** 62,68 ****

  }

  

  

! /* Return non-zero iff HEADER is a prefix of TEXT.  HEADER should be

     null-terminated; LEN is the length of TEXT.  */

  static int

  match_header (char *header, char *text, size_t len)

--- 62,69 ----

  }

  

  

! /* Return non-zero iff HEADER is a prefix of TEXT, ignoring

!    differences in case.  HEADER should be lower-case, and

     null-terminated; LEN is the length of TEXT.  */

  static int

  match_header (char *header, char *text, size_t len)

***************

*** 76,81 ****

--- 77,84 ----

    for (i = 0; i < header_len; i++)

      {

        char t = text[i];

+       if ('A' <= t && t <= 'Z')

+         t += 'a' - 'A';

        if (header[i] != t)

          return 0;

      }

$ 


This output takes a bit of effort to get used to, but it is definitely worth understanding.

The interesting portion starts with the first two lines beginning with *** and ---; those describe the older and newer files compared. The remainder consists of two hunks, each of which starts with a line of asterisks. Here is the first hunk:


***************

*** 62,68 ****

  }

  

  

! /* Return non-zero iff HEADER is a prefix of TEXT.  HEADER should be

     null-terminated; LEN is the length of TEXT.  */

  static int

  match_header (char *header, char *text, size_t len)

--- 62,69 ----

  }

  

  

! /* Return non-zero iff HEADER is a prefix of TEXT, ignoring

!    differences in case.  HEADER should be lower-case, and

     null-terminated; LEN is the length of TEXT.  */

  static int

  match_header (char *header, char *text, size_t len)


Text from the older version appears after the *** 62,68 *** line; text from the new appears after the --- 62,69 --- line. The pair of numbers in each indicates the range of lines shown. CVS provides context around the change, and marks the actual lines affected with `!' characters. Thus, one can see that the single line in the top half was replaced with the two lines in the bottom half.

Here is the second hunk:


***************

*** 76,81 ****

--- 77,84 ----

    for (i = 0; i < header_len; i++)

      {

        char t = text[i];

+       if ('A' <= t && t <= 'Z')

+         t += 'a' - 'A';

        if (header[i] != t)

        return 0;

      }


This hunk describes the insertion of two lines, marked with `+' characters. CVS omits the old text in this case, because it would be redundant. CVS uses a similar hunk format to describe deletions.

Like the Unix diff command, output from cvs diff is usually called a patch, because developers have traditionally used the format to distribute bug fixes or small new features. While reasonably readable to humans, a patch contains enough information for a program to apply the changes it describes to an unmodified text file. In fact, the Unix patch command does exactly this, given a patch as input.

Adding and deleting files

CVS treats file creation and deletion like other changes, recording such events in the files' histories. One way to look at this is to say that CVS records the history of directories as well as the files they contain.

CVS doesn't assume that newly created files should be placed under its control; this would do the wrong thing in many circumstances. For example, one needn't record changes to object files and executables, since their contents can always be recreated from the source files (one hopes). Instead, if you create a new file, cvs update will flag it with a `?' character until you tell CVS what you intend to do with it.

To add a file to a project, you must first create the file, and then use the cvs add command to mark it for addition. Then, the next call to cvs commit will add the file to the repository. For example, here's how you might add a README file to the PYLINUS project:


$ ls

CVS          Makefile     linusc.c      poll-server

$ vi README

... enter a description of linusc ...

$ ls

CVS          Makefile     README       linusc.c      poll-server

$ cvs update

cvs update: Updating .

? README --- CVS doesn't know about this file yet.

$ cvs add README

cvs add: scheduling file `README' for addition

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

$ cvs update --- Now what does CVS think?

cvs update: Updating .

A README --- The file is marked for addition.

$ cvs commit README

... CVS prompts you for a log entry ...

RCS file: /opt/cvsroot/linus/PYLINUS/README,v

done

Checking in README;

/opt/cvsroot/linus/PYLINUS/README,v  <--  README

initial revision: 1.1

done

$


CVS treats deleted files similarly. If you delete a file and then run cvs update, CVS doesn't assume you intend the file for deletion. Instead, it does something benign -- it recreates the file with its last recorded contents, and flags it with a `U' character, as for any other update. (This means that if you want to undo the changes you've made to a file in your working directory, you can simply delete the files, and then let cvs update recreate them.)

To remove a file from a project, you must first delete the file, and then use the cvs rm command to mark it for deletion. Then, the next call to cvs commit will delete the file from the repository.

Committing a file marked with cvs rm does not destroy the file's history. It simply adds a new revision, which is marked as "non-existent." The repository still has records of the file's prior contents, and can recall them as needed -- for example, by cvs diff or cvs log.

There are several strategies for renaming files; the simplest is to simply rename the file in your working directory, and run cvs rm on the old name, and cvs add on the new name. The disadvantage of this approach is that the log entries for the old file's content do not carry over to the new file. Other strategies avoid this quirk, but have other, stranger problems.

You can add directories just as you would ordinary files;

Writing good log entries

If one can use cvs diff to retrieve the actual text of a change, why should one bother writing a log entry? Obviously, log entries can be shorter than a patch, and allow the reader to get a general understanding of the change without delving into its details.

However, a good log entry describes the reason the developer made the change. For example, a bad log entry for revision 1.7 shown above might say, "Convert t to lower-case." This would be accurate, but completely useless; cvs diff provides all the same information, more clearly. A better log entry would be, "Make this test case-insensitive," because it makes the purpose clear to anyone with a general understanding of the code: HTTP clients should ignore case differences when parsing reply headers.

Handling conflicts

As mentioned above, the cvs update command incorporates changes made by other developers into your working directory. If both you and another developer have modified the same file, CVS merges their changes with yours.

It's straightforward to imagine how this works when the changes apply to distant regions of a file, but what happens when you and another developer have changed the same line? CVS calls this situation a conflict, and leaves it up to you to resolve it.

For example, suppose that you have just added some error checking to the host name lookup code. Before you commit your change, you must run cvs update, to bring your sources into sync:


$ cvs update

cvs update: Updating .

RCS file: /u/src/master/linusc/linusc.c,v

retrieving revision 1.8

retrieving revision 1.9

Merging differences between 1.8 and 1.9 into linusc.c

rcsmerge: warning: conflicts during merge

cvs update: conflicts found in linusc.c

C linusc.c

$ 


In this case, another developer has changed the same region of the file you have, so CVS complains about a conflict. Instead of printing `M linusc.c', as it usually does, it prints `C linusc.c', to indicate that a conflict has occurred in that file.

To resolve the conflict, bring up the file in your editor. CVS marks the conflicting text this way:


  /* Look up the IP address of the host.  */

  host_info = gethostbyname (hostname);

<<<<<<< linusc.c

  if (! host_info)

    {

      fprintf (stderr, "%s: host not found: %s\n", progname, hostname);

      exit (1);

    }

=======

  if (! host_info)

    {

      printf ("linusc: no host");

      exit (1);

    }

>>>>>>> 1.9



  sock = socket (PF_INET, SOCK_STREAM, 0);


It's important to understand what CVS does and doesn't consider a conflict. CVS does not understand the semantics of your program; it simply treats its source code as a tree of text files. If one developer adds a new argument to a function and fixes its callers, while another developer simultaneously adds a new call to that function, and does not pass the new argument, that is certainly a conflict -- the two changes are incompatible -- but CVS will not report it. CVS's understanding of conflicts is strictly textual.

In practice, fortunately, conflicts are rare. Usually, they seem to result from two developers attempting to address the same problem, a lack of communication between developers, or disagreement about the design of the program. Allocating tasks to developers in a reasonable way reduces the likelihood of conflicts.

Many version control systems allow a developer to lock a file, preventing others from making changes to it until she has committed her changes. While locking is appropriate in some situations, it is not clearly a better solution than the approach CVS takes. Changes can usually be merged correctly, and developers occasionally forget to release locks; in both cases, explicit locking causes unnecessary delays. Furthermore, locks prevent only textual conflicts; they do not prevent semantic conflicts of the sort described above, if the two developers make their changes to different files.

Adding new projects to the repository

When you begin using CVS, you will probably already have several projects that can be put under CVS control. In these cases the easiest way is to use the import command. An example is probably the easiest way to explain how to use it. If the files you want to install in CVS reside in `Pycluster-v1.28', and you want them to appear in the repository as `$CVSROOT/cluster/Pycluster', you can do this:

 
$ cd Pycluster-v1.28

$ cvs import -m "Imported sources" cluster/Pycluster pycluster start

Unless you supply a log message with the `-m' flag, CVS starts an editor and prompts for a message. The string `pycluster' is a vendor tag, and `start' is a release tag. They may fill no purpose in this context, but since CVS requires them they must be present.

You can now verify that it worked, and remove your original source directory.

 
$ cd ..
$ cvs checkout cluster/Pycluster       # Explanation below
$ diff -r Pycluster-v1.28 cluster/Pycluster
$ rm -r Pycluster-v1.28

Erasing the original sources is a good idea, to make sure that you do not accidentally edit them in Pycluster-v1.28, bypassing CVS. Of course, it would be wise to make sure that you have a backup of the sources before you remove them.