[Kernel, courtesy IowaFarmer.com CornCam]

CS 235 Advanced Operating Systems, Winter 2008

CVS Hints

You'll be developing enough software in this course that it's worth your time to learn a source code management (SCM) system, which lets you track versions of your source files over time, branch development, and merge changes between branches. This page provides a quick, CS 235-centric introduction to the most widely used open-source SCM, CVS, the Concurrent Versions System. (But you can use whatever source code management system you'd like; some other popular choices include Subversion, Perforce [commercial], BitKeeper [commercial], and GNU Arch.)

What is source code management?

A source code management system isn't just about source code. Think of it as a file system with explicit versions. The SCM stores the current version of your files, but it also stores every previous version of your files, back to the very first version you entered. Commands called checkouts let you create copies of the current version of the code, or any previous version. The versioning is explicit because you must explicitly tell the SCM to store a new version of a file; this is called committing the file.

There are a lot of reasons this is a good idea! If you mess up some code, you can go back to a previous version. You can see what your code looked like on a previous date, and compare it with today's code. You can tag a particular version with a name, and you can even maintain several branches of the code. A branch is a set of versions that develops independently. For example, after a major product release, the source code is often branched into a "release branch". This means that the main line of source development, which is called the "main trunk" or "mainline", can evolve independently from the release, and updates such as new features won't destabilize the release branch. If bugs are found in the release, they are fixed on the release branch as well as mainline. Another use for branching is to let several developers evolve the code independently, then merge their changes when they're ready. Large-scale software development often uses this branch-and-merge model. In CS 235, you'll use a branch-and-merge model with two branches: mainline, your version of the code, and a vendor branch that contains our original lab files.

Setting up a CVS repository for Lab 1

The first step is to decide where your CVS repository will go. This is the place where CVS will store all the old versions of your files. If you're doing all your work on SEASnet, use your SEASnet home directory; if you're doing your work on a home computer, put it there. Whichever home directory you choose, we suggest you put your CVS repository in ~/cvsroot (a cvsroot subdirectory of your home directory). Then tell CVS where to find the repository:

For tcsh and other csh-like shells     For bash and other sh-like shells
% setenv CVSROOT $HOME/cvsroot          
$ export CVSROOT=$HOME/cvsroot         

(If you're not sure which kind of shell you're using, try the tcsh version on the left. If it fails with an error like "bash: setenv: command not found", use the bash version on the right.) This sets the environment variable CVS uses to find your repository. We suggest putting this statement in your login files (.tcshrc or .bashrc, respectively) so you don't have to type it over and over again. Then tell CVS to initialize the repository:

% cvs init

You can look in the repository if you'd like; there's not much going on.

% ls ~/cvsroot
CVSROOT

Now it's time to download our lab1.tar.gz tarball and add it to the repository. We use the gzcat and tar programs to unpack the lab code, then the cvs import command to add it to our new repository.

% gzcat lab1.tar.gz | tar xf -
% cd lab1
% ls
CODING  GNUmakefile  boot  conf  grade.sh  inc  kern  lib  mergedep.pl
% cvs import -m "lab1 import" jos LAB LAB1
N jos/mergedep.pl
N jos/GNUmakefile
N jos/.bochsrc
...
N jos/boot/Makefrag
N jos/boot/boot.S
N jos/boot/main.c

No conflicts created by this import

(If you use GNU tar, you can just say "tar xzf lab1.tar.gz" in the first step.) Let's take apart the cvs import command, which is where all the magic happens.

  • cvs import -- CVS's import subcommand is used to import existing source code into a repository.
  • -m "lab1 import" -- CVS requires you to supply a reason for most actions that change the repository. These checkin messages are a useful audit trail, and sometimes will be the only way you can figure out what you were thinking when you made some change! The -m option lets you supply a checkin message on the command line. If you left off the option, CVS would put up a text editor and ask you to enter a message there.
  • jos -- This names the module you're importing. Our operating system is called JOS, so we named the module jos. This means that in ordinary circumstances, when you check the operating system code out of the repository it will be in a directory called jos.
  • LAB -- The second argument is a symbolic tag name for the vendor branch, which will contain our original handout code with none of your changes. What you call this doesn't really matter, as long as you are always consistent. Here we chose the name LAB.
  • LAB1 -- The third argument is a tag for this particular version of the software being imported. We chose LAB1 because we are importing the sources for lab 1. We will use this name to refer to this particular version of the software.

What's going on inside the repository?

% ls ~/cvsroot
CVSROOT  jos
% ls ~/cvsroot/jos
CODING,v       boot  grade.sh,v  kern  mergedep.pl,v
GNUmakefile,v  conf  inc         lib

All the files from the lab1 directory have versions in ~/cvsroot/jos, but don't edit the weird ,v files directly; that would mess up the versioning. Instead, check out the jos module to create a working copy.

% cd ~
% cvs co jos
U jos/.bochsrc
U jos/CODING
U jos/GNUmakefile
...
U jos/lib/printfmt.c
U jos/lib/readline.c
U jos/lib/string.c
% ls jos
CODING  CVS  GNUmakefile  boot  conf  grade.sh  inc  kern  lib  mergedep.pl

The CVS co command stands for "checkout". You specify the name of a module to check out, which in this case is jos. CVS then extracts a working copy of the software from the repository (by default the latest version on mainline, but you can specify other branches or versions with the -r flag). It creates a directory called jos containing that working copy. Go ahead and browse the directory. It looks exactly like the contents of the lab1 tarball, except that there are extra directories called CVS where CVS keeps some extra information about what exactly you have checked out. You shouldn't ordinarily mess with the contents of CVS directories.

Making changes

You've now imported your code into a CVS repository, and checked out a working copy of the repository code. There's no need for the lab1 directory any more, so go ahead and remove that. All of your edits for the lab should take place within the jos working copy.

So say you got some work done on the kern/monitor.c source file, and you'd like to commit a version of that file back in to the repository. Here's how. From inside the jos directory:

% cvs ci -m 'mon_backtrace() improvements'
Checking in kern/monitor.c;
/home/kohler/cvsroot/jos/kern/monitor.c,v  <--  monitor.c
new revision: 1.2; previous revision: 1.1
done

As before, the -m option supplies a checkin message; if you'd left it off, CVS would have put up an editor.

Use the cvs diff command to compare two different versions of the code. For example, let's see how my mon_backtrace() compares with the original version I imported as LAB1.

% cvs diff -u -r LAB1
Index: kern/monitor.c
===================================================================
RCS file: /home/kohler/foo/jos/kern/monitor.c,v
retrieving revision 1.1.1.1
retrieving revision 1.2
diff -u -u -r1.1.1.1 -r1.2
--- kern/monitor.c      3 Oct 2005 05:38:25 -0000       1.1.1.1
+++ kern/monitor.c      3 Oct 2005 06:18:59 -0000       1.2
@@ -59,6 +59,9 @@
 mon_backtrace(int argc, char **argv, struct Trapframe *tf)
 {
        // Your code here.
+
+       /* Code code code code code code code code! */
+
        return 0;
 }

The options are:

  • -r LAB1 -- The -r option names a revision. Here, we have told diff to compare the LAB1 revision with the code that's currently in the working copy. You can also give two -r options to compare one version with another, or supply -D to name the version as of a particular date.
  • -u -- Creates a "unified" diff. This kind of diff shows the two versions mixed together. The "@@" line tells you the line numbers where the change begins: at line 59 in both the old version and the new version. In the change description itself, old lines are preceded by a "-" sign, new lines by a "+" sign, and lines that are the same in both versions by a space. The diff above shows that we've added 3 lines since the LAB1 version. Also try -c for a "context" diff.

Some other CVS commands you need to know about:

  • cvs add file1 [file2 ...]
    When you add new files to the source tree, CVS will not automatically incorporate them into the repository. You must tell it you want to include the files with the cvs add command. Note that just executing this command does not change the repository. The add will only take effect the next time you issue a commit (cvs ci).
  • cvs rm file1 [file2 ...]
    Similarly, to delete a file from the repository, you must first delete the working copy, then issue the cvs rm command, then issue a commit command. (Of course, old versions will still be available in the CVS repository.) If you just delete the copy in the working directory by accident, you can always just get it back by issuing a cvs up command.
  • cvs log file
    Gives you a log of changes to a particular file. For example, try cvs log grade.sh. Note that internally CVS uses numeric revision numbers for files. However, these numbers are not consistent across files. For example, the latest version of grade.sh will probably be 1.1, while the latest version of monitor.c is 1.2. That's why we need symbolic tags. The symbolic tags are listed at the top of the cvs log output, even though the actual log entries are listed by revision number.

Updating labs

When we release another lab -- or a lab bugfix -- you'll import the new lab sources into your vendor branch. In the rest of this section, we'll assume you're updating from Lab 1 to Lab 2, and want to merge your solutions with our updates, but the same principles will apply elsewhere. Here's what it will look like graphically:

                                     +------+
   Mainline          +--> work on -->| SOL1 |---+---> merged -->...
                     |     Lab 1     +------+   |
                     |                          |
                  +------+                   +------+
   Vendor branch  | LAB1 |------------------>| LAB2 |
   (-r LAB)       +------+                   +------+

The first step is to make sure your changes are checked in to the repository with cvs ci. You may also want to tag your solutions for Lab 1, so you can easily refer to that version of the lab files. To do that, type cvs tag SOL1 in your working copy directory. This attaches a symbolic tag name to the revision that corresponds to the working copy. Now you can use -r SOL1 to refer to your Lab 1 solutions, and cvs diff -u -r SOL1 will show any changes you've made since those solutions. Make sure your changes are checked in before you run cvs tag!

The actual import uses the same command as for the initial checkin, but with a different tag. To incorporate lab2 into your repository:

% gzcat lab2.tar.gz | tar xf -
% cd lab2
% cvs import -m "lab2 import" jos LAB LAB2
U jos/mergedep.pl
U jos/GNUmakefile
U jos/.bochsrc
...
N jos/kern/trapentry.S
C jos/kern/monitor.c
U jos/kern/console.c
U jos/kern/COPYRIGHT
...
U jos/boot/boot.S
U jos/boot/main.c

1 conflicts created by this import.
Use the following command to help the merge:

        cvs -d /home/kohler/cvsroot checkout -j -jLAB2 jos

The LAB tag for the vendor branch must be the same as for your initial import, but the LAB2 release tag must be different than any previous release tag. Several files seem to have been added in this lab (they are listed with the N character) and several others were updated (the U character).

There's also a conflict: jos/kern/monitor.c is listed with a C character. (You may see a different set of conflicts.) Import conflicts happen when a file was changed both locally, on mainline, and on the vendor branch. In this case, I changed the kern/monitor.c file in the process of solving Lab 1, and the lab vendor changed the same file between LAB1 and LAB2. This may or may not indicate a true conflict; CVS will tell you later. You should resolve these conflicts before continuing, and CVS has given you a pretty good hint of how to do it!

Change into your updated working copy directory and use the update command to merge the vendor branch's changes. As CVS suggested, we'll supply two tags to the cvs up command. This will merge all changes from between those tags into our working copy. This command tends to be pretty verbose.

% cd ~/jos
% cvs up -Pd -jLAB1 -jLAB2
grade.sh already contains the differences between 1.1.1.1 and 1.1.1.2
conf/lab.mk already contains the differences between 1.1.1.1 and 1.1.1.2
inc/trap.h already contains the differences between creation and 1.1.1.1
kern/init.c already contains the differences between 1.1.1.1 and 1.1.1.2
kern/kclock.c already contains the differences between creation and 1.1.1.1
kern/kclock.h already contains the differences between creation and 1.1.1.1
RCS file: /home/kohler/class/ujos/cvsch_cvsroot/jos/kern/monitor.c,v
retrieving revision 1.1.1.1
retrieving revision 1.1.1.2
Merging differences between 1.1.1.1 and 1.1.1.2 into monitor.c
kern/pmap.c already contains the differences between creation and 1.1.1.1
kern/pmap.h already contains the differences between creation and 1.1.1.1
kern/trap.c already contains the differences between creation and 1.1.1.1
kern/trap.h already contains the differences between creation and 1.1.1.1
kern/trapentry.S already contains the differences between creation and 1.1.1.1
lib/readline.c already contains the differences between 1.1.1.1 and 1.1.1.2
user/testpmap.c already contains the differences between creation and 1.1.1.1

Now take a look and see if any of the files have changed. cvs up does this well:

% cvs up
M kern/monitor.c

CVS has merged changes into kern/monitor.c! Use cvs diff to examine the differences, and then cvs ci to check them back in to mainline.

Sometimes, though, CVS will not know how to resolve a difference between the two branches. It reports this as a conflict during cvs up, and won't let you check the conflict in to the repository. For example:

% cvs up
C kern/monitor.c

You must resolve these conflicts by hand. CVS changes the conflicted file to show you both versions. Combine them as appropriate, remove the conflict markers, and check in the result. Here's an example conflict:

// Simple command-line kernel monitor useful for
// controlling the kernel and exploring the system interactively.

#include <inc/stdio.h>
#include <inc/string.h>
#include <inc/memlayout.h>
#include <inc/assert.h>
#include <inc/x86.h>

#include <kern/console.h>
#include <kern/monitor.h>
#include <kern/kdebug.h>
<<<<<<< monitor.c
#include <inc/string.h>
=======
#include <kern/trap.h>
>>>>>>> 1.1.1.2

#define CMDBUF_SIZE	80	// enough for one VGA text line

struct Command { ...

The red lines are the conflict markers. The <<<<<<< line marks the beginning of the conflict. CVS tells you that this version, between <<<<<<< and =======, corresponds to the old working copy ("monitor.c"). The second version, between ======= and >>>>>>>, corresponds to the repository version -- in this case, LAB2. It looks like the vendor branch added an #include file (<kern/trap.h>), and you added a different #include file at the same place. The right resolution is to simply include them both. So you should remove the conflict markers, leaving the file like this:

// Simple command-line kernel monitor useful for
// controlling the kernel and exploring the system interactively.

#include <inc/stdio.h>
#include <inc/string.h>
#include <inc/memlayout.h>
#include <inc/assert.h>
#include <inc/x86.h>

#include <kern/console.h>
#include <kern/monitor.h>
#include <kern/kdebug.h>
#include <inc/string.h>
#include <kern/trap.h>

#define CMDBUF_SIZE	80	// enough for one VGA text line

struct Command { ...

Then check in the result. Although this was a particularly simple conflict to resolve, the principles are the same even for far more complex conflicts.

For more information

See the CVS "Cederqvist" manual, also available in GNU info on most Unix systems that have CVS installed.

This page will be updated with more information as the quarter progresses.

Written with the help of David Mazières's version

Back to CS 235 Advanced Operating Systems, Winter 2008