--- scmtools/cvs.py	(revision 0)
+++ scmtools/cvs.py	(revision 0)
@@ -0,0 +1,93 @@
+import re
+import subprocess
+
+from reviewboard.scmtools.core import SCMError, FileNotFoundError, \
+                                      SCMTool, HEAD, PRE_CREATION
+from reviewboard.diffviewer.parser import DiffParser, DiffParserError
+
+class CVSTool(SCMTool):
+    regex_rev = re.compile(r'^.*?(\d+(\.\d+)+)\r?$')
+
+    def __init__(self, repository):
+        try:
+            self.repopath = repository.path.split(':')[1]
+            self.CVSROOT = ":pserver:%s@%s" % (repository.username, repository.path)
+        except IndexError:
+            self.repopath = self.CVSROOT = repository.path
+        self.client = CVSClient(self.CVSROOT)
+
+        SCMTool.__init__(self, repository)
+
+    def get_file(self, path, revision=HEAD):
+        if not path:
+            raise FileNotFoundError(path, revision)
+
+        return self.client.cat_file(path, revision)
+
+    def parse_diff_revision(self, file_str, revision_str):
+        if revision_str == "PRE-CREATION":
+            return file_str, PRE_CREATION
+
+        m = self.regex_rev.match(revision_str)
+        if not m:
+            raise SCMError("Unable to parse diff revision header '%s'" %
+                           revision_str)
+        return file_str, m.group(1)
+
+    def get_diffs_use_absolute_paths(self):
+        return True
+
+    def get_fields(self):
+        return ['diff_path']
+
+    def getParser(self, data):
+        return CVSDiffParser(data, self.repopath)
+
+class CVSDiffParser(DiffParser):
+    """
+        This class is able to parse diffs created with CVS.
+    """
+
+    regex_small = re.compile('^RCS file: (.+)$')
+
+    def __init__(self, data, repo):
+        DiffParser.__init__(self, data)
+        self.regex_full = re.compile('^RCS file: %s/(.*),v$' % re.escape(repo))
+
+    def _parseRevisionInfo(self, linenum, file):
+        m = self.regex_full.match(self.lines[linenum-3])
+        if not m:
+            m = self.regex_small.match(self.lines[linenum-2])
+
+        if m:
+            filename = m.group(1)
+        else:
+            raise DiffParserError('Unable to find RCS line around line "%d"' % linenum)
+
+        try:
+            file.origFile, file.origInfo = self.lines[linenum].split(None, 2)[1:]
+            file.newFile,  file.newInfo  = self.lines[linenum + 1].split(None, 2)[1:]
+            if file.origFile == '/dev/null':
+                file.origFile, file.origInfo = (file.newFile, 'PRE-CREATION')
+            else:
+                file.origFile = filename
+        except ValueError:
+            raise DiffParserError("The diff file is missing revision information")
+
+
+class CVSClient:
+    def __init__(self, repository):
+        self.repository = repository
+
+    def cat_file(self, filename, revision):
+        p = subprocess.Popen(['cvs', '-d', self.repository, 'checkout',
+                              '-r', str(revision), '-p', filename],
+                             stderr=subprocess.PIPE, stdout=subprocess.PIPE,
+                             close_fds=True)
+        contents = p.stdout.read()
+        errmsg = p.stderr.read()
+        failure = p.wait()
+
+        if failure:
+            raise FileNotFoundError(errmsg)
+        return contents
--- scmtools/testdata/cvs_repo/CVSROOT/cvswrappers	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/cvswrappers	(revision 0)
@@ -0,0 +1,19 @@
+# This file affects handling of files based on their names.
+#
+# The -m option specifies whether CVS attempts to merge files.
+#
+# The -k option specifies keyword expansion (e.g. -kb for binary).
+#
+# Format of wrapper file ($CVSROOT/CVSROOT/cvswrappers or .cvswrappers)
+#
+#  wildcard	[option value][option value]...
+#
+#  where option is one of
+#  -f		from cvs filter		value: path to filter
+#  -t		to cvs filter		value: path to filter
+#  -m		update methodology	value: MERGE or COPY
+#  -k		expansion mode		value: b, o, kkv, &c
+#
+#  and value is a single-quote delimited value.
+# For example:
+#*.gif -k 'b'
--- scmtools/testdata/cvs_repo/CVSROOT/cvswrappers,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/cvswrappers,v	(revision 0)
@@ -0,0 +1,42 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# This file affects handling of files based on their names.
+#
+# The -m option specifies whether CVS attempts to merge files.
+#
+# The -k option specifies keyword expansion (e.g. -kb for binary).
+#
+# Format of wrapper file ($CVSROOT/CVSROOT/cvswrappers or .cvswrappers)
+#
+#  wildcard	[option value][option value]...
+#
+#  where option is one of
+#  -f		from cvs filter		value: path to filter
+#  -t		to cvs filter		value: path to filter
+#  -m		update methodology	value: MERGE or COPY
+#  -k		expansion mode		value: b, o, kkv, &c
+#
+#  and value is a single-quote delimited value.
+# For example:
+#*.gif -k 'b'
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/taginfo	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/taginfo	(revision 0)
@@ -0,0 +1,20 @@
+# The "taginfo" file is used to control pre-tag checks.
+# The filter on the right is invoked with the following arguments:
+#
+# $1 -- tagname
+# $2 -- operation "add" for tag, "mov" for tag -F, and "del" for tag -d
+# $3 -- repository
+# $4->  file revision [file revision ...]
+#
+# A non-zero exit of the filter program will cause the tag to be aborted.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being committed to, relative
+# to the $CVSROOT.  For the first match that is found, then the remainder
+# of the line is the name of the filter to run.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
--- scmtools/testdata/cvs_repo/CVSROOT/taginfo,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/taginfo,v	(revision 0)
@@ -0,0 +1,43 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "taginfo" file is used to control pre-tag checks.
+# The filter on the right is invoked with the following arguments:
+#
+# $1 -- tagname
+# $2 -- operation "add" for tag, "mov" for tag -F, and "del" for tag -d
+# $3 -- repository
+# $4->  file revision [file revision ...]
+#
+# A non-zero exit of the filter program will cause the tag to be aborted.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being committed to, relative
+# to the $CVSROOT.  For the first match that is found, then the remainder
+# of the line is the name of the filter to run.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/loginfo	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/loginfo	(revision 0)
@@ -0,0 +1,26 @@
+# The "loginfo" file controls where "cvs commit" log information
+# is sent.  The first entry on a line is a regular expression which must match
+# the directory that the change is being made to, relative to the
+# $CVSROOT.  If a match is found, then the remainder of the line is a filter
+# program that should expect log information on its standard input.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name ALL appears as a regular expression it is always used
+# in addition to the first matching regex or DEFAULT.
+#
+# You may specify a format string as part of the
+# filter.  The string is composed of a `%' followed
+# by a single format character, or followed by a set of format
+# characters surrounded by `{' and `}' as separators.  The format
+# characters are:
+#
+#   s = file name
+#   V = old version number (pre-checkin)
+#   v = new version number (post-checkin)
+#
+# For example:
+#DEFAULT (echo ""; id; echo %s; date; cat) >> $CVSROOT/CVSROOT/commitlog
+# or
+#DEFAULT (echo ""; id; echo %{sVv}; date; cat) >> $CVSROOT/CVSROOT/commitlog
--- scmtools/testdata/cvs_repo/CVSROOT/loginfo,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/loginfo,v	(revision 0)
@@ -0,0 +1,49 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "loginfo" file controls where "cvs commit" log information
+# is sent.  The first entry on a line is a regular expression which must match
+# the directory that the change is being made to, relative to the
+# $CVSROOT.  If a match is found, then the remainder of the line is a filter
+# program that should expect log information on its standard input.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name ALL appears as a regular expression it is always used
+# in addition to the first matching regex or DEFAULT.
+#
+# You may specify a format string as part of the
+# filter.  The string is composed of a `%' followed
+# by a single format character, or followed by a set of format
+# characters surrounded by `{' and `}' as separators.  The format
+# characters are:
+#
+#   s = file name
+#   V = old version number (pre-checkin)
+#   v = new version number (post-checkin)
+#
+# For example:
+#DEFAULT (echo ""; id; echo %s; date; cat) >> $CVSROOT/CVSROOT/commitlog
+# or
+#DEFAULT (echo ""; id; echo %{sVv}; date; cat) >> $CVSROOT/CVSROOT/commitlog
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/checkoutlist	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/checkoutlist	(revision 0)
@@ -0,0 +1,13 @@
+# The "checkoutlist" file is used to support additional version controlled
+# administrative files in $CVSROOT/CVSROOT, such as template files.
+#
+# The first entry on a line is a filename which will be checked out from
+# the corresponding RCS file in the $CVSROOT/CVSROOT directory.
+# The remainder of the line is an error message to use if the file cannot
+# be checked out.
+#
+# File format:
+#
+#	[<whitespace>]<filename>[<whitespace><error message>]<end-of-line>
+#
+# comment lines begin with '#'
--- scmtools/testdata/cvs_repo/CVSROOT/editinfo	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/editinfo	(revision 0)
@@ -0,0 +1,21 @@
+# The "editinfo" file is used to allow verification of logging
+# information.  It works best when a template (as specified in the
+# rcsinfo file) is provided for the logging procedure.  Given a
+# template with locations for, a bug-id number, a list of people who
+# reviewed the code before it can be checked in, and an external
+# process to catalog the differences that were code reviewed, the
+# following test can be applied to the code:
+#
+#   Making sure that the entered bug-id number is correct.
+#   Validating that the code that was reviewed is indeed the code being
+#       checked in (using the bug-id number or a seperate review
+#       number to identify this particular code set.).
+#
+# If any of the above test failed, then the commit would be aborted.
+#
+# Actions such as mailing a copy of the report to each reviewer are
+# better handled by an entry in the loginfo file.
+#
+# One thing that should be noted is the the ALL keyword is not
+# supported.  There can be only one entry that matches a given
+# repository.
--- scmtools/testdata/cvs_repo/CVSROOT/history	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/history	(revision 0)
@@ -0,0 +1 @@
+O46a87353|Robin|~/devel/test1/*0|test||test
--- scmtools/testdata/cvs_repo/CVSROOT/rcsinfo	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/rcsinfo	(revision 0)
@@ -0,0 +1,13 @@
+# The "rcsinfo" file is used to control templates with which the editor
+# is invoked on commit and import.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being made to, relative to the
+# $CVSROOT.  For the first match that is found, then the remainder of the
+# line is the name of the file that contains the template.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
--- scmtools/testdata/cvs_repo/CVSROOT/checkoutlist,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/checkoutlist,v	(revision 0)
@@ -0,0 +1,36 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "checkoutlist" file is used to support additional version controlled
+# administrative files in $CVSROOT/CVSROOT, such as template files.
+#
+# The first entry on a line is a filename which will be checked out from
+# the corresponding RCS file in the $CVSROOT/CVSROOT directory.
+# The remainder of the line is an error message to use if the file cannot
+# be checked out.
+#
+# File format:
+#
+#	[<whitespace>]<filename>[<whitespace><error message>]<end-of-line>
+#
+# comment lines begin with '#'
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/editinfo,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/editinfo,v	(revision 0)
@@ -0,0 +1,44 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "editinfo" file is used to allow verification of logging
+# information.  It works best when a template (as specified in the
+# rcsinfo file) is provided for the logging procedure.  Given a
+# template with locations for, a bug-id number, a list of people who
+# reviewed the code before it can be checked in, and an external
+# process to catalog the differences that were code reviewed, the
+# following test can be applied to the code:
+#
+#   Making sure that the entered bug-id number is correct.
+#   Validating that the code that was reviewed is indeed the code being
+#       checked in (using the bug-id number or a seperate review
+#       number to identify this particular code set.).
+#
+# If any of the above test failed, then the commit would be aborted.
+#
+# Actions such as mailing a copy of the report to each reviewer are
+# better handled by an entry in the loginfo file.
+#
+# One thing that should be noted is the the ALL keyword is not
+# supported.  There can be only one entry that matches a given
+# repository.
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/commitinfo	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/commitinfo	(revision 0)
@@ -0,0 +1,15 @@
+# The "commitinfo" file is used to control pre-commit checks.
+# The filter on the right is invoked with the repository and a list 
+# of files to check.  A non-zero exit of the filter program will 
+# cause the commit to be aborted.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being committed to, relative
+# to the $CVSROOT.  For the first match that is found, then the remainder
+# of the line is the name of the filter to run.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
--- scmtools/testdata/cvs_repo/CVSROOT/config	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/config	(revision 0)
@@ -0,0 +1,21 @@
+# Set this to "no" if pserver shouldn't check system users/passwords
+#SystemAuth=no
+
+# Put CVS lock files in this directory rather than directly in the repository.
+#LockDir=/var/lock/cvs
+
+# Set `TopLevelAdmin' to `yes' to create a CVS directory at the top
+# level of the new working directory when using the `cvs checkout'
+# command.
+#TopLevelAdmin=no
+
+# Set `LogHistory' to `all' or `TOEFWUPCGMAR' to log all transactions to the
+# history file, or a subset as needed (ie `TMAR' logs all write operations)
+#LogHistory=TOEFWUPCGMAR
+
+# Set `RereadLogAfterVerify' to `always' (the default) to allow the verifymsg
+# script to change the log message.  Set it to `stat' to force CVS to verify# that the file has changed before reading it (this can take up to an extra
+# second per directory being committed, so it is not recommended for large
+# repositories.  Set it to `never' (the previous CVS behavior) to prevent
+# verifymsg scripts from changing the log message.
+#RereadLogAfterVerify=always
--- scmtools/testdata/cvs_repo/CVSROOT/rcsinfo,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/rcsinfo,v	(revision 0)
@@ -0,0 +1,36 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "rcsinfo" file is used to control templates with which the editor
+# is invoked on commit and import.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being made to, relative to the
+# $CVSROOT.  For the first match that is found, then the remainder of the
+# line is the name of the file that contains the template.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/commitinfo,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/commitinfo,v	(revision 0)
@@ -0,0 +1,38 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "commitinfo" file is used to control pre-commit checks.
+# The filter on the right is invoked with the repository and a list 
+# of files to check.  A non-zero exit of the filter program will 
+# cause the commit to be aborted.
+#
+# The first entry on a line is a regular expression which is tested
+# against the directory that the change is being committed to, relative
+# to the $CVSROOT.  For the first match that is found, then the remainder
+# of the line is the name of the filter to run.
+#
+# If the repository name does not match any of the regular expressions in this
+# file, the "DEFAULT" line is used, if it is specified.
+#
+# If the name "ALL" appears as a regular expression it is always used
+# in addition to the first matching regex or "DEFAULT".
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/config,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/config,v	(revision 0)
@@ -0,0 +1,44 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# Set this to "no" if pserver shouldn't check system users/passwords
+#SystemAuth=no
+
+# Put CVS lock files in this directory rather than directly in the repository.
+#LockDir=/var/lock/cvs
+
+# Set `TopLevelAdmin' to `yes' to create a CVS directory at the top
+# level of the new working directory when using the `cvs checkout'
+# command.
+#TopLevelAdmin=no
+
+# Set `LogHistory' to `all' or `TOEFWUPCGMAR' to log all transactions to the
+# history file, or a subset as needed (ie `TMAR' logs all write operations)
+#LogHistory=TOEFWUPCGMAR
+
+# Set `RereadLogAfterVerify' to `always' (the default) to allow the verifymsg
+# script to change the log message.  Set it to `stat' to force CVS to verify# that the file has changed before reading it (this can take up to an extra
+# second per directory being committed, so it is not recommended for large
+# repositories.  Set it to `never' (the previous CVS behavior) to prevent
+# verifymsg scripts from changing the log message.
+#RereadLogAfterVerify=always
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/modules	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/modules	(revision 0)
@@ -0,0 +1,26 @@
+# Three different line formats are valid:
+#	key	-a    aliases...
+#	key [options] directory
+#	key [options] directory files...
+#
+# Where "options" are composed of:
+#	-i prog		Run "prog" on "cvs commit" from top-level of module.
+#	-o prog		Run "prog" on "cvs checkout" of module.
+#	-e prog		Run "prog" on "cvs export" of module.
+#	-t prog		Run "prog" on "cvs rtag" of module.
+#	-u prog		Run "prog" on "cvs update" of module.
+#	-d dir		Place module in directory "dir" instead of module name.
+#	-l		Top-level directory only -- do not recurse.
+#
+# NOTE:  If you change any of the "Run" options above, you'll have to
+# release and re-checkout any working directories of these modules.
+#
+# And "directory" is a path to a directory relative to $CVSROOT.
+#
+# The "-a" option specifies an alias.  An alias is interpreted as if
+# everything on the right of the "-a" had been typed on the command line.
+#
+# You can encode a module within a module by using the special '&'
+# character to interpose another module into the current module.  This
+# can be useful for creating a module that consists of many directories
+# spread out over the entire source repository.
--- scmtools/testdata/cvs_repo/CVSROOT/notify	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/notify	(revision 0)
@@ -0,0 +1,12 @@
+# The "notify" file controls where notifications from watches set by
+# "cvs watch add" or "cvs edit" are sent.  The first entry on a line is
+# a regular expression which is tested against the directory that the
+# change is being made to, relative to the $CVSROOT.  If it matches,
+# then the remainder of the line is a filter program that should contain
+# one occurrence of %s for the user to notify, and information on its
+# standard input.
+#
+# "ALL" or "DEFAULT" can be used in place of the regular expression.
+#
+# For example:
+#ALL mail -s "CVS notification" %s
--- scmtools/testdata/cvs_repo/CVSROOT/modules,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/modules,v	(revision 0)
@@ -0,0 +1,49 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# Three different line formats are valid:
+#	key	-a    aliases...
+#	key [options] directory
+#	key [options] directory files...
+#
+# Where "options" are composed of:
+#	-i prog		Run "prog" on "cvs commit" from top-level of module.
+#	-o prog		Run "prog" on "cvs checkout" of module.
+#	-e prog		Run "prog" on "cvs export" of module.
+#	-t prog		Run "prog" on "cvs rtag" of module.
+#	-u prog		Run "prog" on "cvs update" of module.
+#	-d dir		Place module in directory "dir" instead of module name.
+#	-l		Top-level directory only -- do not recurse.
+#
+# NOTE:  If you change any of the "Run" options above, you'll have to
+# release and re-checkout any working directories of these modules.
+#
+# And "directory" is a path to a directory relative to $CVSROOT.
+#
+# The "-a" option specifies an alias.  An alias is interpreted as if
+# everything on the right of the "-a" had been typed on the command line.
+#
+# You can encode a module within a module by using the special '&'
+# character to interpose another module into the current module.  This
+# can be useful for creating a module that consists of many directories
+# spread out over the entire source repository.
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/notify,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/notify,v	(revision 0)
@@ -0,0 +1,35 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "notify" file controls where notifications from watches set by
+# "cvs watch add" or "cvs edit" are sent.  The first entry on a line is
+# a regular expression which is tested against the directory that the
+# change is being made to, relative to the $CVSROOT.  If it matches,
+# then the remainder of the line is a filter program that should contain
+# one occurrence of %s for the user to notify, and information on its
+# standard input.
+#
+# "ALL" or "DEFAULT" can be used in place of the regular expression.
+#
+# For example:
+#ALL mail -s "CVS notification" %s
+@
+
--- scmtools/testdata/cvs_repo/CVSROOT/verifymsg	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/verifymsg	(revision 0)
@@ -0,0 +1,21 @@
+# The "verifymsg" file is used to allow verification of logging
+# information.  It works best when a template (as specified in the
+# rcsinfo file) is provided for the logging procedure.  Given a
+# template with locations for, a bug-id number, a list of people who
+# reviewed the code before it can be checked in, and an external
+# process to catalog the differences that were code reviewed, the
+# following test can be applied to the code:
+#
+#   Making sure that the entered bug-id number is correct.
+#   Validating that the code that was reviewed is indeed the code being
+#       checked in (using the bug-id number or a seperate review
+#       number to identify this particular code set.).
+#
+# If any of the above test failed, then the commit would be aborted.
+#
+# Actions such as mailing a copy of the report to each reviewer are
+# better handled by an entry in the loginfo file.
+#
+# One thing that should be noted is the the ALL keyword is not
+# supported.  There can be only one entry that matches a given
+# repository.
--- scmtools/testdata/cvs_repo/CVSROOT/verifymsg,v	(revision 0)
+++ scmtools/testdata/cvs_repo/CVSROOT/verifymsg,v	(revision 0)
@@ -0,0 +1,44 @@
+head     1.1;
+access   ;
+symbols  ;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.47.57;  author Robin;  state Exp;
+branches;
+next     ;
+
+desc
+@@
+
+
+
+1.1
+log
+@initial checkin@
+text
+@# The "verifymsg" file is used to allow verification of logging
+# information.  It works best when a template (as specified in the
+# rcsinfo file) is provided for the logging procedure.  Given a
+# template with locations for, a bug-id number, a list of people who
+# reviewed the code before it can be checked in, and an external
+# process to catalog the differences that were code reviewed, the
+# following test can be applied to the code:
+#
+#   Making sure that the entered bug-id number is correct.
+#   Validating that the code that was reviewed is indeed the code being
+#       checked in (using the bug-id number or a seperate review
+#       number to identify this particular code set.).
+#
+# If any of the above test failed, then the commit would be aborted.
+#
+# Actions such as mailing a copy of the report to each reviewer are
+# better handled by an entry in the loginfo file.
+#
+# One thing that should be noted is the the ALL keyword is not
+# supported.  There can be only one entry that matches a given
+# repository.
+@
+
--- scmtools/testdata/cvs_repo/test/testfile,v	(revision 0)
+++ scmtools/testdata/cvs_repo/test/testfile,v	(revision 0)
@@ -0,0 +1,40 @@
+head     1.1;
+branch   1.1.1;
+access   ;
+symbols  initial:1.1.1.1 reviewboard:1.1.1;
+locks    ; strict;
+comment  @# @;
+
+
+1.1
+date     2007.07.26.08.50.30;  author Robin;  state Exp;
+branches 1.1.1.1;
+next     ;
+
+1.1.1.1
+date     2007.07.26.08.50.30;  author Robin;  state Exp;
+branches ;
+next     ;
+
+
+desc
+@@
+
+
+
+1.1
+log
+@Initial revision
+@
+text
+@test content
+@
+
+
+1.1.1.1
+log
+@Initial import.
+
+@
+text
+@@
--- scmtools/fixtures/initial_data.json	(revision 817)
+++ scmtools/fixtures/initial_data.json	(working copy)
@@ -1 +1 @@
-[{"pk": "1", "model": "scmtools.tool", "fields": {"name": "Subversion", "class_name": "reviewboard.scmtools.svn.SVNTool"}}, {"pk": "2", "model": "scmtools.tool", "fields": {"name": "Perforce", "class_name": "reviewboard.scmtools.perforce.PerforceTool"}}]
+[{"pk": "1", "model": "scmtools.tool", "fields": {"name": "Subversion", "class_name": "reviewboard.scmtools.svn.SVNTool"}}, {"pk": "2", "model": "scmtools.tool", "fields": {"name": "Perforce", "class_name": "reviewboard.scmtools.perforce.PerforceTool"}}, {"pk": "3", "model": "scmtools.tool", "fields": {"class_name": "reviewboard.scmtools.cvs.CVSTool", "name": "CVS"}}]
--- scmtools/tests.py	(revision 817)
+++ scmtools/tests.py	(working copy)
@@ -8,6 +8,7 @@
 except ImportError:
     pass
 
+from reviewboard.diffviewer.parser import DiffParserError
 from reviewboard.scmtools.core import SCMError, FileNotFoundError, \
                                       Revision, HEAD, PRE_CREATION, \
                                       ChangeSet
@@ -29,6 +30,104 @@
         self.assert_(len(cs.files) == 0)
 
 
+class CVSTests(unittest.TestCase):
+    """Unit tests for CVS."""
+
+    def setUp(self):
+        self.cvs_repo_path = os.path.join(os.path.dirname(__file__),
+                                          'testdata/cvs_repo')
+        self.repository = Repository(name='CVS',
+                                     path=self.cvs_repo_path,
+                                     tool=Tool.objects.get(name='CVS'))
+
+        try:
+            self.tool = self.repository.get_scmtool()
+        except ImportError:
+            raise nose.SkipTest
+
+    def testGetFile(self):
+        """Testing CVSTool.get_file"""
+        expected = "test content\n"
+        file = 'test/testfile'
+        rev = Revision('1.1')
+
+        self.assertEqual(self.tool.get_file(file, rev), expected)
+
+        self.assert_(self.tool.file_exists('test/testfile'))
+        self.assert_(not self.tool.file_exists('test/testfile2'))
+
+        self.assertRaises(FileNotFoundError,
+                          lambda: self.tool.get_file(''))
+        self.assertRaises(FileNotFoundError,
+                          lambda: self.tool.get_file('hello', PRE_CREATION))
+
+    def testRevisionParsing(self):
+        """Testing revision number parsing"""
+        self.assertEqual(self.tool.parse_diff_revision('', 'PRE-CREATION')[1],
+                         PRE_CREATION)
+        self.assertEqual(self.tool.parse_diff_revision('', '7 Nov 2005 13:17:07 -0000	1.2')[1],
+                         '1.2')
+        self.assertEqual(self.tool.parse_diff_revision('', '7 Nov 2005 13:17:07 -0000	1.2.3.4')[1],
+                         '1.2.3.4')
+        self.assertRaises(SCMError,
+                          lambda: self.tool.parse_diff_revision('', 'hello'))
+
+    def testInterface(self):
+        """Testing basic CVSTool API"""
+        self.assertEqual(self.tool.get_diffs_use_absolute_paths(), True)
+        self.assertEqual(self.tool.get_fields(), ['diff_path'])
+
+    def testSimpleDiff(self):
+        """Testing parsing CVS simple diff"""
+        diff = "Index: testfile\n==========================================" + \
+               "=========================\nRCS file: %s/test/testfile,v\nre" + \
+               "trieving revision 1.1.1.1\ndiff -u -r1.1.1.1 testfile\n--- " + \
+               "testfile    26 Jul 2007 08:50:30 -0000      1.1.1.1\n+++ te" + \
+               "stfile    26 Jul 2007 10:20:20 -0000\n@@ -1 +1,2 @@\n-test " + \
+               "content\n+updated test content\n+added info"
+
+        file = self.tool.getParser(diff % self.cvs_repo_path).parse()[0]
+        self.assertEqual(file.origFile, 'test/testfile')
+        self.assertEqual(file.origInfo, '26 Jul 2007 08:50:30 -0000      1.1.1.1')
+        self.assertEqual(file.newFile, 'testfile')
+        self.assertEqual(file.newInfo, '26 Jul 2007 10:20:20 -0000')
+        self.assertEqual(len(file.data), 161)
+
+    def testBadDiff(self):
+        """Testing parsing CVS diff with bad info"""
+        diff = "Index: newfile\n===========================================" + \
+               "========================\ndiff -N newfile\n--- /dev/null	1" + \
+               "Jan 1970 00:00:00 -0000\n+++ newfile	26 Jul 2007 10:11:45 " + \
+               "-0000\n@@ -0,0 +1 @@\n+new file content"
+
+        self.assertRaises(DiffParserError,
+                          lambda: self.tool.getParser(diff).parse())
+
+    def testBadDiff2(self):
+        """Testing parsing CVS bad diff with new file"""
+        diff = "Index: newfile\n===========================================" + \
+               "========================\nRCS file: newfile\ndiff -N newfil" + \
+               "e\n--- /dev/null\n+++ newfile	2" + \
+               "6 Jul 2007 10:11:45 -0000\n@@ -0,0 +1 @@\n+new file content"
+
+        self.assertRaises(DiffParserError,
+                          lambda: self.tool.getParser(diff).parse())
+
+    def testNewfileDiff(self):
+        """Testing parsing CVS diff with new file"""
+        diff = "Index: newfile\n===========================================" + \
+               "========================\nRCS file: newfile\ndiff -N newfil" + \
+               "e\n--- /dev/null	1 Jan 1970 00:00:00 -0000\n+++ newfile	2" + \
+               "6 Jul 2007 10:11:45 -0000\n@@ -0,0 +1 @@\n+new file content"
+
+        file = self.tool.getParser(diff).parse()[0]
+        self.assertEqual(file.origFile, 'newfile')
+        self.assertEqual(file.origInfo, 'PRE-CREATION')
+        self.assertEqual(file.newFile, 'newfile')
+        self.assertEqual(file.newInfo, '26 Jul 2007 10:11:45 -0000')
+        self.assertEqual(len(file.data), 111)
+
+
 class SubversionTests(unittest.TestCase):
     """Unit tests for subversion."""
 
