Sunday, January 01, 2006

Version Control with RCS

We Pragmatic Programmers know that putting our source code under version control is important. I use CVS for version control both at work and my open source stuff at sourceforge. But there are times when I work on my laptop without checking in for days or weeks at a time. At these times, I rely on RCS, a pessimistic version control system that ships as part of the Unix (and Linux) operating systems. By pessimistic, I mean that a checkout will lock the file, which cannot be checked out by someone else until the file is checked back in. This is in contrast to the optimistic style favored by CVS and Subversion, where multiple authors can check out the same file at the same time, and any reconciliation is done at the time the changes are checked in.

Unlike CVS or Subversion, RCS does not need a server component, so there is no additional daemon to run on your laptop. Simply create an RCS subdirectory under the directory you want to put under version control , and you can use the standard RCS commands to put files under version control. To checkout a file, use the command:

ci -l MyFile.java

and this will create a version controlled file RCS/MyFile.java,v. The ci (check-in) is slightly misnamed in this example, since it also locks the file and checks it out at the same time in one command. There are other commands like rcsdiff, rlog and ident, which allow you to see the differences between two revisions of a file, display a log of all commit comments and get a display ident information for a file respectively. For a complete list, checkout:

man ci

and follow the links to the other commands.

One thing that was holding me back from using RCS was the need to remember an entirely new set of commands for version control. I have been using CVS for so long that I have developed finger memory. Also it was not clear to me how to apply a command to a hierarchy of files. Since I develop mostly in Java, and Java code is organized into a hierarchy of packages, this is important functionality to me.

For these reasons, I developed a script in Python which provides me functionality which is fairly close to that in CVS. Here is the help output from the script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Usage: rcstool.py [add|diff|log|commit|help] [-m=comment] [filename]
 add [dirname] - puts a new directory under RCS control. If no
                 dirname is specified, then puts the current directory
                 under RCS control
 diff [filename] - Reports differences between all files under
                 the current directory and the RCS version, or if a filename
                 is specified, the actual differences between the file and
                 its RCS version
 log filename - Prints the RCS log for the specified file
 commit -m=comment [filename]+ - Specifies a list of files that
                 needs to be checked into RCS. The comment to put in all
                 files is specified by preceding with -m=. Note that multi-
                 word comments should be enclosed in quotes
 help - print this message

The add is analogous to the CVS add subcommand, the diff to -nq update (in the no filename supplied mode) and diff (where the filename is supplied), the log to log and the commit to commit, respectively. More importantly, the script will traverse the file system from the root and apply the command to each file it encounters.

Here is the script (rcstool.py) for those interested. You will notice that rcstool.py has been put under RCS version control as well :-). You will need the Python interpreter installed to runA this. Most Red Hat Linux distributions will have this pre-installed, since Python is used for writing and running the sysadmin GUI tools.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/python
# $Id: rcstool.py,v 1.3 2005/12/24 01:01:24 sujit Exp sujit $
# $Source: /home/sujit/bin/python/RCS/rcstool.py,v $
"""
Provides a CVS like wrapper for RCS with common commands. Please make sure
to modify the commands RLOG, RCSDIFF and COMMIT to conform to your local
system.
"""
import sys
import os
 
# Specify locations of the various commands you want to use here
RLOG = "/usr/bin/rlog"
RCSDIFF = "/usr/bin/rcsdiff"
COMMIT = "/usr/bin/ci -l"
 
def main():
    """
    This is how we are called.
    """
    if (len(sys.argv) == 1 or sys.argv[1] == "help"):
        help()
    elif (sys.argv[1] == "add"):
        adddir = os.getcwd()
        if (len(sys.argv) == 3):
            adddir = sys.argv[2]
        add(adddir)
    elif (sys.argv[1] == "diff"):
        filename = "*"
        if (len(sys.argv) == 3):
           filename = sys.argv[2]
        diff(filename)
    elif (sys.argv[1] == "log"):
        if (len(sys.argv) != 3):
            help()
        log(sys.argv[2])
    elif (sys.argv[1] == "commit"):
        print len(sys.argv)
        if (len(sys.argv) < 4):
            help()
        comment = sys.argv[2]
        if not comment.startswith("-m="):
            help()
        filenames = sys.argv[3:]
        commit(comment, filenames)
    else:
        help()
 
def add(adddir):
    """
    Creates an RCS directory under the specified directory. If there is
    already an RCS directory, it prints an error message and returns.
    """
    rcsdir = os.path.join(adddir, "RCS")
    if (os.path.isdir(rcsdir)):
        print "This directory is already under RCS control"
    else:
        os.mkdir(rcsdir)
 
def diff(filename):
    """
    Reports on diffs between the actual and RCS version. If no filename is
    supplied, it will list the files that are different from the RCS version.
    If the filename is supplied, then the actual differences from RCS are
    displayed for that file.
    """
    if (filename == "*"):
        visitFiles(os.getcwd(), RCSDIFF, 0)
    else:
        diff = os.popen(" ".join([RCSDIFF, filename]), 'r')
        for result in diff.readlines():
            print result
        diff.close()
 
def log(filename):
    """
    Prints the RCS log for the specified filename
    """
    log = os.popen(" ".join([RLOG, filename]), 'r')
    for result in log.readlines():
        print result
    log.close()
 
def commit(comment, filenames):
    """
    Commits a list of supplied filenames, with the appropriate commit comment.
    Our preferred mode of checking in is "ci -l", which locks the file again
    after check-in, so its always writable.
    """
    commitMessage = "-m=\"" + comment[3:] + "\""
    command = [COMMIT, commitMessage]
    for filename in filenames:
        command.append(filename)
    commit = os.popen(" ".join(command), 'r')
    for result in commit.readlines():
        print result
 
def visitFiles(root, operation, level):
    """
    Generic recursive directory walk. Applies the operation to each of the
    files encountered in the walk. This version is customized to ignore RCS
    files and any file which ends with ",v" (RCS file extensions). This
    version also ignores files on which the rcs operation is not required.
    Since we use this for commit and diff, only the files which are different
    from the RCS version.
    """
    for filename in os.listdir(root):
        fullpath = os.path.join(root, filename)
        if (os.path.isfile(fullpath)):
            oper = os.popen(" ".join([operation, fullpath, "2>/dev/null"]), 'r')
            numlines = 0
            for result in oper.readlines():
                numlines = numlines + 1
            if (numlines > 0):
                print fullpath
            oper.close()
        else:
            if filename == "RCS":
                continue
            visitFiles(fullpath, operation, level + 1)
 
def help():
    """
    Simple usage help text that prints and exits.
    """
    print "Usage: rcstool.py [add|diff|log|commit|help] [-m=comment] [filename]"    
    print "  add [dirname] - puts a new directory under RCS control. If no"
    print "       dirname is specified, then puts the current directory"
    print "       under RCS control"
    print "  diff [filename] - Reports differences between all files under"
    print "       the current directory and the RCS version, or if a filename"
    print "       is specified, the actual differences between the file and"
    print "       its RCS version"
    print "  log filename - Prints the RCS log for the specified file"
    print "  commit -m=comment [filename]+ - Specifies a list of files that"
    print "       needs to be checked into RCS. The comment to put in all "
    print "       files is specified by preceding with -m=. Note that multi-"
    print "       word comments should be enclosed in quotes"
    print "  help - print this message"
    sys.exit(-1)
 
if __name__ == "__main__":
    main()

Update: I dont use this anymore. As pragmatic as it seemed when I started writing this, it turned out to be too inconvenient to learn another set of commands.

Be the first to comment. Comments are moderated to prevent spam.