Paul Heinlein
First published on November 8, 2004
Last updated on March 16, 2005


RCS is a Revision Control System. It manages changes in text documents, automating the storage, retrieval, logging, and logging of file revisions. It’s a great tool for any sort of file that passes for source code: HTML files, published documents, programming code, etc.

The RCS system consists of several binary applications: ci, co, ident, rcs, rcsdiff, rcsmerge, and rlog. The Fedora Core, Red Hat, Gentoo, and Debian distributions all include RCS in a package called (oddly enough) “rcs.”

The typical recipe for using RCS is quite short.

# get to where you're going
cd /path/to/workingdirectory

# check out and lock your file
co -l workfile

# use editor of choice to work on file
$EDITOR workfile

# see what's changed in your latest version
rcsdiff workfile

# check in the new version of your file
ci -u workfile

The rest of this article will explain and expand upon that simple procedure.

An example session

I think the best way to show you how to use RCS is to run through a relatively complete example session. This example won’t by any means use all of RCS’s capabilities, but it’ll provide a decent base of knowledge and practice.

Create a work directory

For the purposes of this example, I’m going to call the directory in which we’ll do our work $HOME/src or ~/src. There’s nothing important about that directory name; feel free to adjust it to your needs.

So, first off, we’ll create it and make it our working directory.

mkdir $HOME/src
cd $HOME/src

Next, we’ll create a directory with a name that is important and that shouldn’t be changed.

mkdir RCS

Having a directory named RCS in your work directory isn’t mandatory, but if it’s there RCS will use it to store is working files. I find it much cleaner to have RCS do its housekeeping in a subdirectory rather than in my work directory.

A sample file

Now we need a sample file. Let’s create a short shell script that acts like the world’s favorite example file.

# foo.sh -- a silly example that acts in an oddly familiar way
# $Id$
# $Source$
echo "Hello, world!"

You’ll notice the three non-standard lines, one with the RCS keyword $Id$, the other with $Source$. The co(1) man page has a KEYWORD SUBSTITUTION section that lists all the keywords you can use, but these will give you a flavor for what’s available.

A listing of our work directory now reveals one file and one subdirectory:

$ /bin/ls -og
total 8
drwxr-xr-x  2 4096 Nov  5 14:09 RCS
-rw-r--r--  1  118 Nov  5 14:14 foo.sh

Initial check-in

We’ll check the file into RCS using the ci application. The default check-in process makes the working file “disappear.” There’s the revision-control version in the RCS directory, but otherwise the file is invisible. I usually like to be able to see my working files (since I rarely maintain multiple working versions of the same file), so I almost always check files in using the -u option.

ci -u foo.sh

RCS will spring to life and ask you to provide an overall description of the file you’re checking in. The text of the description can span multiple lines; you signal the end of the text with line that consists of nothing but a period.

$ ci -u foo.sh
RCS/foo.sh,v  <--  foo.sh
enter description, terminated with single '.' or end of file:
NOTE: This is NOT the log message!
>> a silly, yet familiar, example
>> .
initial revision: 1.1

There are three file changes now worth observing.

  1. The working file is now marked read-only by the filesystem.

    $ /bin/ls -og
    total 8
    drwxr-xr-x  2 4096 Nov  7 20:19 RCS
    -r--r--r--  1  200 Nov  7 20:16 foo.sh
  2. In the RCS directory is a file named foo.sh,v. It is the source file that will (eventually, at least) keep track of all the changes made to your working file.

  3. The RCS keyword tags in your working file now contain revision information.

    $ cat foo.sh
    # foo.sh -- a silly example that acts in an oddly familiar way
    # $Id: foo.sh,v 1.1 2004/11/08 04:19:33 heinlein Exp $
    # $Source: /home/heinlein/src/RCS/foo.sh,v $
    echo "Hello, world!"

    The $Id$ tag now contains the number of the revision (1.1), the date and time that revision was checked in (expressed in UTC unless you say otherwise), and the username of the person who checked in that version.

    The $Source$ tag contains the full path to the source file.

Check out and edit

After some time goes by, we become dissatisfied with our little script. We’re not always ready to send the world our hearty greeting. It’s time to check the file out and work on it a bit. You’ll almost always want to lock the files you check out, using the -l option.

co -l foo.sh

When you ckeck out a file, RCS will report the version on the file you’re editing.

$ co -l foo.sh
RCS/foo.sh,v  -->  foo.sh
revision 1.1 (locked)

When the file is checked out in this manner, the filesystem will once again report that it’s writable.

$ /bin/ls -og
total 8
drwxr-xr-x  2 4096 Nov  7 20:20 RCS
-rw-r--r--  1  209 Nov  7 20:20 foo.sh

We decide we need to rule out happy greetings on our least favorite day of the week, so we adjust our script accordingly. At the end of our editing session, we use the rcsdiff utility to make sure we introduced no unwanted changes into our working file.

$ rcsdiff foo.sh
RCS file: RCS/foo.sh,v
retrieving revision 1.1
diff -r1.1 foo.sh
< echo "Hello, world!"
> if [ $(date +%A) = 'Monday' ]; then
>   echo "Is it Friday yet?"
> else
>   echo "Hello, world!"
> fi

Checking in the changes

Satisfied with our latest editing efforts, we check the file back in, once again using the -u option so ensure that our script is still visible after check-in.

ci -u foo.sh

During the check-in process, RCS will prompt us for a log message that describes the current changes.

$ ci -u foo.sh
RCS/foo.sh,v  <--  foo.sh
new revision: 1.2; previous revision: 1.1
enter log message, terminated with single '.' or end of file:
>> "Hello, world!" on Monday? Feh.
>> .

That’s it. Just rinse, lather, and repeat as you continue to improve and enhance your work file. If you ever want to recall the file’s revision history, just use the rlog application.

rlog workfile

Incrementing version numbers

By default, RCS will only increment the minor numbers in your version number: 1.1, 1.2, 1.3, 1.4, … At some point, however, you’ll want to release version two. Just tell ci which version number you want to start using the -r option.

$ ci -r2 -u foo.sh
RCS/foo.sh,v  <--  foo.sh
new revision: 2.1; previous revision: 1.2
enter log message, terminated with single '.' or end of file:
>> rev 2 objectives met. customer happy.
>> .

You’ll note that if you don’t supply a minor number for your release, RCS will assume that it’s .1. You can, if you’re so inclined, start with .0 by specifying it, e.g., ci -r2.0 -u foo.sh.

Bulk check-ins

I often find myself wanting to check out an entire set of files to make a similar change in all of them, like altering a directory path or URL they all reference.

Checking out a bunch of files is easy to automate since the co utility requires no user intervention.

Likewise, making a common change in a set of files is easy using sed, awk, or dozens of other scripting tools.

The more obscure part is automating the check-in process, since typically the ci utility requires a log entry for each file.

The answer is to use the -m option or to pipe the log message to RCS. Either method will do the trick.

# using -m; no space allowed between option and the message
ci -u -m'altered name of main image file' *.html

# using stdin
echo 'altered name of main image file' | ci -u *.html

Date stamps

RCS stores datestamps in UTC (aka GMT) time—and by default displays them that way—which can be confusing at first. Here on the American west coast, we’re seven or eight hours behind UTC, depending on the time of year. If I check in a file today at 5:30 p.m., the RCS date stamp will show the file checked in tomorrow at 12:30 a.m. (during daylight savings time) or 1:30 a.m. (if we’re on standard time).

RCS provides the -z option to override the time zone displayed in the date stamp. The ci(1) man page has the full details, but essentially you can specify either a time offset or the string LT (indicating local time).

# check in a file using the local timezone
ci -u -zLT workfile

# the same, using an explicit offset
ci -u -z-08:00 workfile

Also by default, RCS uses the working file’s last modification time as the stamped date and time. That can be overridden using the -d option. See the co(1) man page for several more examples and a list of acceptable date formats.

# using a full ISO 8601 UTC timestamp
ci -u -d'2004-11-17 06:30:00+00' workfile

# using date(1) format
ci -u -d'Sun Nov 14 06:30:23 PST 2004' workfile

# the same, using GNU date and command substitution
ci -u -d"$(date -d '3 days ago')" workfile

RCS in emacs

I divide my editing time between vim and emacs; I’m pretty neutral in the Text Editor Wars. One handy feature of emacs, however, is its integrated RCS support. It’s possible to check files in and out of RCS from within the editor itself.

I use this feature constantly, but I’ll admit that I don’t use the the full breadth of emacs’ version-control features. Instead, I typically just check files out and back in again.

If you use emacs to open a file currently under RCS control, you should see “RCS” and the file’s version number in the mode line near the bottom of your buffer, e.g., RCS:1.5.

After that, the recipe is pretty easy.

Use C-x v v (vc-next-action) to check out the file. By default, this implies locking the file as you’d do with co -l.

After you’ve edited the file, you check in your changes using the same keystrokes: C-x v v. You get a new buffer in which to add a log comment. Once you’re finished with the comment, type C-c C-c to commit the changes. By default, this implies checking in the file without a lock, like ci -u.

It’s worth noting that emacs won’t save the customary backup file (e.g., foo.c~) when working in RCS mode. If something goes haywire, you’ll have to revert to the last good checked-in version of your working file.

Migrating from RCS to CVS

I recently had the occasion to move a large number of files from RCS to CVS. Since both systems use the same underlying libraries—indeed, CVS is built on top of RCS—they can use the same source files.

The biggest migration issues are getting rid of anything in your directory tree that’s not a source file and making sure that all your source files are currently checked in. The following script will handle the migration, though it assumes shell-friendly directory and filenames (it’ll fail if directory names include spaces).

# warning: this script does precious little error checking. bwahahaa!

# season these variables to taste

cd "$SRCDIR"

# identify any unlocked files
UNLOCKED=$(grep '^locks' $(find . -type f -name '*,v') | grep -v 'locks;')
if test -n "$UNLOCKED"; then
  printf \
    "%d files are unlocked and should be checked in:\n" \
    $(echo "$UNLOCKED" | wc -l)
  echo "$UNLOCKED" | sed 's!:locks$!!'
  exit 1
  printf "All existing source files are checked in. Excellent!\n\n"

# copy all the ,v source files to destination; identify RCS
# directories, move source files out of them up a directory, and
# delete the RCS directories.
test -d "$DESTDIR" || mkdir -p "$DESTDIR"
printf "Copying source files to %s\n" "$DESTDIR"
tar -cf - $(find . -type f -name '*,v') | (cd "$DESTDIR" && tar -xvf -)
printf "Removing any RCS directories\n"
for dir in $(find . -type d -name RCS); do
  (cd $dir && mv *,v ..)
  rmdir -v $dir
printf "\n%s is now CVS-ready\n" "$DESTDIR"

If the script is successful, your destination directory can now be copied as-is into your CVSROOT.