[viewvc-dev] [PATCHES] A pile of cleanup patches for rcsparse

Michael Haggerty <[email protected]>
Newsgroups gmane.comp.version-control.cvs.viewcvs.devel
Message-ID <4735F3C0.30106__10989.4631001682$1194718177$gmane$org@alum.mit.edu>
Hi,

I can hardly remember my original goal...oh yeah, I want to add support
to rcsparse for RCS newphrases.  In the course of poking around the
code, I have been making minor changes and cleanups.  I have appended a
"series" file telling the order of the patches, and the patches
themselves.  The patches apply to viewvc trunk r1710.  If you save all
of the files into a directory called "patches" (relative to the root of
the viewvc source tree), then you can manage the patches using quilt(1).

Most of the patches are just refactoring, but the following ones are
actually useful :-) :

parse-rcs-file.diff + add-tests.diff: Add a very simple testing
framework for rcsparse, including one test.  (Run "python run-tests.py"
from the rcsparse directory.)

fix-warning.diff: Fix an error in tparse/tparse.h that prevented
compilation on my computer.

allow-empty-head.diff + allow-empty-tree.diff: Handle RCS files that
don't contain any revisions at all.  (This is legal RCS, believe it or not.)

I haven't attempted to make the same changes to tparse, partly because
tparse segfaults for me so I can't even run it.

Let me know what you think, especially about whether tparse necessarily
has to be kept synchronized with rcsparse.

Michael

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
series (text/plain, 539 B)
parse-rcs-file.diff -p0
add-tests.diff -p0
fix-warning.diff -p0
sort-sink-callbacks.diff -p0
separate-strict.diff -p0
token-lookup.diff -p0
remove-dead-eof-check.diff -p0
allow-empty-head.diff -p0
allow-empty-tree.diff -p0
simplify-match.diff -p0
better-rcs-expected.diff -p0
parse-raises-rcs-expected.diff -p0
simplify-semicolon-handling.diff -p0
rename-locals.diff -p0
simplify-parse-admin-access.diff -p0
parse-rcs-tree-entry.diff -p0
simplify-tree-entry-parsing.diff -p0
simplify-date-processing.diff -p0
read-until-semicolon.diff -p0
parse-rcs-file.diff (text/x-diff, 2.9 KB)
Add a new test/demonstration script.

Submitted by: Michael Haggerty <[email protected]>

This script was taken from the cvs2svn project, http://cvs2svn.tigris.org.

* lib/vclib/ccvs/rcsparse/parse_rcs_file.py: New demonstration script;
  illustrates the use of the rcsparse API.
Index: lib/vclib/ccvs/rcsparse/parse_rcs_file.py
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/parse_rcs_file.py	2007-11-10 15:40:09.000000000 +0100
@@ -0,0 +1,74 @@
+#! /usr/bin/python
+
+# (Be in -*- python -*- mode.)
+#
+# ====================================================================
+# Copyright (c) 2006-2007 CollabNet.  All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.  The terms
+# are also available at http://subversion.tigris.org/license-1.html.
+# If newer versions of this license are posted there, you may use a
+# newer version instead, at your option.
+#
+# This software consists of voluntary contributions made by many
+# individuals.  For exact contribution history, see the revision
+# history and logs, available at http://cvs2svn.tigris.org/.
+# ====================================================================
+
+"""Parse an RCS file, showing the rcsparse callbacks that are called.
+
+This program is useful to see whether an RCS file has a problem (in
+the sense of not being parseable by rcsparse) and also to illuminate
+the correspondence between RCS file contents and rcsparse callbacks.
+
+The output of this program can also be considered to be a kind of
+'canonical' format for RCS files, at least in so far as rcsparse
+returns all relevant information in the file and provided that the
+order of callbacks is always the same."""
+
+
+import sys
+import os
+
+
+class Logger:
+  def __init__(self, f, name):
+    self.f = f
+    self.name = name
+
+  def __call__(self, *args):
+    self.f.write(
+        '%s(%s)\n' % (self.name, ', '.join(['%r' % arg for arg in args]),)
+        )
+
+
+class LoggingSink:
+  def __init__(self, f):
+    self.f = f
+
+  def __getattr__(self, name):
+    return Logger(self.f, name)
+
+
+if __name__ == '__main__':
+  # Since there is nontrivial logic in __init__.py, we can't just
+  # import things directly out of this directory.  Therefore, we
+  # have to add the parent directory to the path, then import
+  # "rcsparse".
+  sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..'))
+
+  import rcsparse
+
+  if sys.argv[1:]:
+    for path in sys.argv[1:]:
+      if os.path.isfile(path) and path.endswith(',v'):
+        rcsparse.parse(
+            open(path, 'rb'), LoggingSink(sys.stdout)
+            )
+      else:
+        sys.stderr.write('%r is being ignored.\n' % path)
+  else:
+    rcsparse.parse(sys.stdin, LoggingSink(sys.stdout))
+
+
add-tests.diff (text/x-diff, 6.2 KB)
Add a very primitive testing infrastructure for rcsparse, and the first test.

* lib/vclib/ccvs/rcsparse/run-tests.py: New file.

* lib/vclib/ccvs/rcsparse/test-data/default,v,
  lib/vclib/ccvs/rcsparse/test-data/default.out: Test data for a
  simple test.
Index: lib/vclib/ccvs/rcsparse/run-tests.py
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/run-tests.py	2007-11-10 15:48:40.000000000 +0100
@@ -0,0 +1,70 @@
+#! /usr/bin/python
+
+# (Be in -*- python -*- mode.)
+#
+# ====================================================================
+# Copyright (c) 2007 CollabNet.  All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.  The terms
+# are also available at http://subversion.tigris.org/license-1.html.
+# If newer versions of this license are posted there, you may use a
+# newer version instead, at your option.
+#
+# This software consists of voluntary contributions made by many
+# individuals.  For exact contribution history, see the revision
+# history and logs, available at http://viewvc.tigris.org/.
+# ====================================================================
+
+"""Run tests of rcsparse code."""
+
+import sys
+import os
+import glob
+from cStringIO import StringIO
+from difflib import Differ
+
+script_dir = os.path.dirname(sys.argv[0])
+sys.path.insert(0, os.path.join(script_dir, '..'))
+
+from rcsparse import parse
+from rcsparse.parse_rcs_file import LoggingSink
+
+
+test_dir = os.path.join(script_dir, 'test-data')
+
+filelist = glob.glob(os.path.join(test_dir, '*,v'))
+filelist.sort()
+
+all_tests_ok = 1
+
+for filename in filelist:
+    sys.stderr.write('%s: ' % (filename,))
+    f = StringIO()
+    try:
+        parse(open(filename, 'rb'), LoggingSink(f))
+    except Exception, e:
+        sys.stderr.write('Error parsing file: %s!\n' % (e,))
+        all_tests_ok = 0
+    else:
+        output = f.getvalue()
+
+        expected_output_filename = filename[:-2] + '.out'
+        expected_output = open(expected_output_filename, 'rb').read()
+
+        if output == expected_output:
+            sys.stderr.write('OK\n')
+        else:
+            sys.stderr.write('Output does not match expected output!\n')
+            differ = Differ()
+            for diffline in differ.compare(
+                expected_output.splitlines(1), output.splitlines(1)
+                ):
+                sys.stderr.write(diffline)
+            all_tests_ok = 0
+
+if all_tests_ok:
+    sys.exit(0)
+else:
+    sys.exit(1)
+
Index: lib/vclib/ccvs/rcsparse/test-data/default,v
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/test-data/default,v	2007-11-10 15:48:50.000000000 +0100
@@ -0,0 +1,102 @@
+head	1.2;
+access;
+symbols
+	B_SPLIT:1.2.0.4
+	B_MIXED:1.2.0.2
+	T_MIXED:1.2
+	B_FROM_INITIALS_BUT_ONE:1.1.1.1.0.4
+	B_FROM_INITIALS:1.1.1.1.0.2
+	T_ALL_INITIAL_FILES_BUT_ONE:1.1.1.1
+	T_ALL_INITIAL_FILES:1.1.1.1
+	vendortag:1.1.1.1
+	vendorbranch:1.1.1;
+locks; strict;
+comment	@# @;
+
+
+1.2
+date	2003.05.23.00.17.53;	author jrandom;	state Exp;
+branches
+	1.2.2.1
+	1.2.4.1;
+next	1.1;
+
+1.1
+date	2003.05.22.23.20.19;	author jrandom;	state Exp;
+branches
+	1.1.1.1;
+next	;
+
+1.1.1.1
+date	2003.05.22.23.20.19;	author jrandom;	state Exp;
+branches;
+next	;
+
+1.2.2.1
+date	2003.05.23.00.31.36;	author jrandom;	state Exp;
+branches;
+next	;
+
+1.2.4.1
+date	2003.06.03.03.20.31;	author jrandom;	state Exp;
+branches;
+next	;
+
+
+desc
+@@
+
+
+1.2
+log
+@Second commit to proj, affecting all 7 files.
+@
+text
+@This is the file `default' in the top level of the project.
+
+Every directory in the `proj' project has a file named `default'.
+
+This line was added in the second commit (affecting all 7 files).
+@
+
+
+1.2.4.1
+log
+@First change on branch B_SPLIT.
+
+This change excludes sub3/default, because it was not part of this
+commit, and sub1/subsubB/default, which is not even on the branch yet.
+@
+text
+@a5 2
+
+First change on branch B_SPLIT.
+@
+
+
+1.2.2.1
+log
+@Modify three files, on branch B_MIXED.
+@
+text
+@a5 2
+
+This line was added on branch B_MIXED only (affecting 3 files).
+@
+
+
+1.1
+log
+@Initial revision
+@
+text
+@d4 2
+@
+
+
+1.1.1.1
+log
+@Initial import.
+@
+text
+@@
Index: lib/vclib/ccvs/rcsparse/test-data/default.out
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/test-data/default.out	2007-11-10 15:49:19.000000000 +0100
@@ -0,0 +1,26 @@
+set_head_revision('1.2')
+define_tag('B_SPLIT', '1.2.0.4')
+define_tag('B_MIXED', '1.2.0.2')
+define_tag('T_MIXED', '1.2')
+define_tag('B_FROM_INITIALS_BUT_ONE', '1.1.1.1.0.4')
+define_tag('B_FROM_INITIALS', '1.1.1.1.0.2')
+define_tag('T_ALL_INITIAL_FILES_BUT_ONE', '1.1.1.1')
+define_tag('T_ALL_INITIAL_FILES', '1.1.1.1')
+define_tag('vendortag', '1.1.1.1')
+define_tag('vendorbranch', '1.1.1')
+set_locking('strict')
+set_comment('# ')
+admin_completed()
+define_revision('1.2', 1053649073, 'jrandom', 'Exp', ['1.2.2.1', '1.2.4.1'], '1.1')
+define_revision('1.1', 1053645619, 'jrandom', 'Exp', ['1.1.1.1'], None)
+define_revision('1.1.1.1', 1053645619, 'jrandom', 'Exp', [], None)
+define_revision('1.2.2.1', 1053649896, 'jrandom', 'Exp', [], None)
+define_revision('1.2.4.1', 1054610431, 'jrandom', 'Exp', [], None)
+tree_completed()
+set_description('')
+set_revision_info('1.2', 'Second commit to proj, affecting all 7 files.\n', "This is the file `default' in the top level of the project.\n\nEvery directory in the `proj' project has a file named `default'.\n\nThis line was added in the second commit (affecting all 7 files).\n")
+set_revision_info('1.2.4.1', 'First change on branch B_SPLIT.\n\nThis change excludes sub3/default, because it was not part of this\ncommit, and sub1/subsubB/default, which is not even on the branch yet.\n', 'a5 2\n\nFirst change on branch B_SPLIT.\n')
+set_revision_info('1.2.2.1', 'Modify three files, on branch B_MIXED.\n', 'a5 2\n\nThis line was added on branch B_MIXED only (affecting 3 files).\n')
+set_revision_info('1.1', 'Initial revision\n', 'd4 2\n')
+set_revision_info('1.1.1.1', 'Initial import.\n', '')
+parse_completed()
fix-warning.diff (text/x-diff, 544 B)
Fix a syntax error that was preventing compilation.

* tparse/tparse.h (rcstoken::init): Fix declaration.
Index: tparse/tparse.h
===================================================================
--- tparse/tparse.h.orig	2007-11-05 21:35:38.000000000 +0100
+++ tparse/tparse.h	2007-11-05 21:35:58.000000000 +0100
@@ -121,7 +121,7 @@
         free(data);
       data = NULL;
     };
-  void rcstoken::init(const char *mydata, size_t mylen);
+  void init(const char *mydata, size_t mylen);
   int null_token()
     {
       return data == NULL;
sort-sink-callbacks.diff (text/x-diff, 3.6 KB)
Reorder definitions in the approximate order that they will be called.

Order the callbacks in Sink and the "if" branched in parse_rcs_admin()
in the approximate order that they will be encountered.

* lib/vclib/ccvs/rcsparse/common.py (Sink): Reorder method definitions.

  (_Parser.parse_rcs_admin): Reorder "if" statement branches.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-05 22:23:18.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-05 23:21:20.000000000 +0100
@@ -18,35 +18,49 @@
 class Sink:
   def set_head_revision(self, revision):
     pass
+
   def set_principal_branch(self, branch_name):
     pass
-  def define_tag(self, name, revision):
-    pass
+
   def set_access(self, accessors):
     pass
-  def set_expansion(self, mode):
+
+  def define_tag(self, name, revision):
+    pass
+
+  def set_locker(self, revision, locker):
     pass
+
   def set_locking(self, mode):
     """Used to signal locking mode.
 
     Called with mode argument 'strict' if strict locking
     Not called when no locking used."""
+
     pass
-  def set_locker(self, revision, locker):
-    pass
+
   def set_comment(self, comment):
     pass
-  def set_description(self, description):
+
+  def set_expansion(self, mode):
     pass
+
+  def admin_completed(self):
+    pass
+
   def define_revision(self, revision, timestamp, author, state,
                       branches, next):
     pass
-  def set_revision_info(self, revision, log, text):
+
+  def tree_completed(self):
     pass
-  def admin_completed(self):
+
+  def set_description(self, description):
     pass
-  def tree_completed(self):
+
+  def set_revision_info(self, revision, log, text):
     pass
+
   def parse_completed(self):
     pass
 
@@ -100,6 +114,15 @@
             self.ts.unget(semi);
           else:
             raise RCSExpected(semi, ';')
+      elif token == "access":
+        accessors = []
+        while 1:
+          tag = self.ts.get()
+          if tag == ';':
+            if accessors != []:
+              self.sink.set_access(accessors)
+            break
+          accessors = accessors + [ tag ]
       elif token == "symbols":
         while 1:
           tag = self.ts.get()
@@ -109,16 +132,6 @@
           tag_name = tag
           tag_rev = self.ts.get()
           self.sink.define_tag(tag_name, tag_rev)
-      elif token == "comment":
-        semi, comment = self.ts.mget(2)
-        self.sink.set_comment(comment)
-        if semi != ';':
-          raise RCSExpected(semi, ';')
-      elif token == "expand":
-        semi, expand_mode = self.ts.mget(2)
-        self.sink.set_expansion(expand_mode)
-        if semi != ';':
-          raise RCSExpected(semi, ';')
       elif token == "locks":
         while 1:
           tag = self.ts.get()
@@ -135,15 +148,16 @@
           self.ts.match(';')
         else:
           self.ts.unget(tag)
-      elif token == "access":
-        accessors = []
-        while 1:
-          tag = self.ts.get()
-          if tag == ';':
-            if accessors != []:
-              self.sink.set_access(accessors)
-            break
-          accessors = accessors + [ tag ]
+      elif token == "comment":
+        semi, comment = self.ts.mget(2)
+        self.sink.set_comment(comment)
+        if semi != ';':
+          raise RCSExpected(semi, ';')
+      elif token == "expand":
+        semi, expand_mode = self.ts.mget(2)
+        self.sink.set_expansion(expand_mode)
+        if semi != ';':
+          raise RCSExpected(semi, ';')
 
       # Chew up "newphrase"
       else:
separate-strict.diff (text/x-diff, 1 KB)
Separate handling of the "strict" keyword into a separate "if" branch.

This makes the keyword handling more uniform.

* lib/vclib/ccvs/rcsparse/common.py (_Parser.parse_rcs_admin):
  Separate handling of the "strict" keyword into a separate "if"
  branch.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-05 23:22:21.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-05 23:22:36.000000000 +0100
@@ -141,13 +141,9 @@
           locker = tag
           rev = self.ts.get()
           self.sink.set_locker(rev, locker)
-
-        tag = self.ts.get()
-        if tag == "strict":
-          self.sink.set_locking("strict")
-          self.ts.match(';')
-        else:
-          self.ts.unget(tag)
+      elif token == "strict":
+        self.sink.set_locking("strict")
+        self.ts.match(';')
       elif token == "comment":
         semi, comment = self.ts.mget(2)
         self.sink.set_comment(comment)
token-lookup.diff (text/x-diff, 5.3 KB)
Rewrite _Parser.parse_rcs_admin() to use a token lookup table.

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_admin_head,
  _Parser._parse_admin_branch, _Parser._parse_admin_access,
  _Parser._parse_admin_symbols, _Parser._parse_admin_locks,
  _Parser._parse_admin_strict, _Parser._parse_admin_comment,
  _Parser._parse_admin_expand): New methods (one for each keyword that
  can appear in the admin section).

  (_Parser.admin_token_map): A map from keyword name to the method
  used to handle that keyword.

  (_Parser.parse_rcs_admin): Look up the keyword tokens in the map
  instead of using a big "if" statement.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-05 23:22:36.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-05 23:56:40.000000000 +0100
@@ -90,75 +90,97 @@
 class _Parser:
   stream_class = None   # subclasses need to define this
 
+  def _parse_admin_head(self, token):
+    semi, rev = self.ts.mget(2)
+    self.sink.set_head_revision(rev)
+    if semi != ';':
+      raise RCSExpected(semi, ';')
+
+  def _parse_admin_branch(self, token):
+    semi, branch = self.ts.mget(2)
+    if semi == ';':
+      self.sink.set_principal_branch(branch)
+    else:
+      if branch == ';':
+        self.ts.unget(semi);
+      else:
+        raise RCSExpected(semi, ';')
+
+  def _parse_admin_access(self, token):
+    accessors = []
+    while 1:
+      tag = self.ts.get()
+      if tag == ';':
+        if accessors != []:
+          self.sink.set_access(accessors)
+        return
+      accessors = accessors + [ tag ]
+
+  def _parse_admin_symbols(self, token):
+    while 1:
+      tag = self.ts.get()
+      if tag == ';':
+        break
+      self.ts.match(':')
+      tag_name = tag
+      tag_rev = self.ts.get()
+      self.sink.define_tag(tag_name, tag_rev)
+
+  def _parse_admin_locks(self, token):
+    while 1:
+      tag = self.ts.get()
+      if tag == ';':
+        break
+      self.ts.match(':')
+      locker = tag
+      rev = self.ts.get()
+      self.sink.set_locker(rev, locker)
+
+  def _parse_admin_strict(self, token):
+    self.sink.set_locking("strict")
+    self.ts.match(';')
+
+  def _parse_admin_comment(self, token):
+    semi, comment = self.ts.mget(2)
+    self.sink.set_comment(comment)
+    if semi != ';':
+      raise RCSExpected(semi, ';')
+
+  def _parse_admin_expand(self, token):
+    semi, expand_mode = self.ts.mget(2)
+    self.sink.set_expansion(expand_mode)
+    if semi != ';':
+      raise RCSExpected(semi, ';')
+
+  admin_token_map = {
+      'head' : _parse_admin_head,
+      'branch' : _parse_admin_branch,
+      'access' : _parse_admin_access,
+      'symbols' : _parse_admin_symbols,
+      'locks' : _parse_admin_locks,
+      'strict' : _parse_admin_strict,
+      'comment' : _parse_admin_comment,
+      'expand' : _parse_admin_expand,
+      }
+
   def parse_rcs_admin(self):
     while 1:
       # Read initial token at beginning of line
       token = self.ts.get()
 
-      # We're done once we reach the description of the RCS tree
-      if token[0] in string.digits:
-        self.ts.unget(token)
-        return
-
-      if token == "head":
-        semi, rev = self.ts.mget(2)
-        self.sink.set_head_revision(rev)
-        if semi != ';':
-          raise RCSExpected(semi, ';')
-      elif token == "branch":
-        semi, branch = self.ts.mget(2)
-        if semi == ';':
-          self.sink.set_principal_branch(branch)
+      try:
+        f = self.admin_token_map[token]
+      except KeyError:
+        # We're done once we reach the description of the RCS tree
+        if token[0] in string.digits:
+          self.ts.unget(token)
+          return
         else:
-          if branch == ';':
-            self.ts.unget(semi);
-          else:
-            raise RCSExpected(semi, ';')
-      elif token == "access":
-        accessors = []
-        while 1:
-          tag = self.ts.get()
-          if tag == ';':
-            if accessors != []:
-              self.sink.set_access(accessors)
-            break
-          accessors = accessors + [ tag ]
-      elif token == "symbols":
-        while 1:
-          tag = self.ts.get()
-          if tag == ';':
-            break
-          self.ts.match(':')
-          tag_name = tag
-          tag_rev = self.ts.get()
-          self.sink.define_tag(tag_name, tag_rev)
-      elif token == "locks":
-        while 1:
-          tag = self.ts.get()
-          if tag == ';':
-            break
-          self.ts.match(':')
-          locker = tag
-          rev = self.ts.get()
-          self.sink.set_locker(rev, locker)
-      elif token == "strict":
-        self.sink.set_locking("strict")
-        self.ts.match(';')
-      elif token == "comment":
-        semi, comment = self.ts.mget(2)
-        self.sink.set_comment(comment)
-        if semi != ';':
-          raise RCSExpected(semi, ';')
-      elif token == "expand":
-        semi, expand_mode = self.ts.mget(2)
-        self.sink.set_expansion(expand_mode)
-        if semi != ';':
-          raise RCSExpected(semi, ';')
-
-      # Chew up "newphrase"
+          # Chew up "newphrase"
+          # warn("Unexpected RCS token: $token\n")
+          pass
       else:
-        pass
-        # warn("Unexpected RCS token: $token\n")
+        f(self, token)
 
     raise RuntimeError, "Unexpected EOF"
remove-dead-eof-check.diff (text/x-diff, 597 B)
Remove dead code.

* lib/vclib/ccvs/rcsparse/common.py (_Parser.parse_rcs_admin): Remove
  dead code (the only way to escape from the loop is via "return").
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 14:02:04.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 14:02:05.000000000 +0100
@@ -182,8 +182,6 @@
       else:
         f(self, token)
 
-    raise RuntimeError, "Unexpected EOF"
-
   def parse_rcs_tree(self):
     while 1:
       revision = self.ts.get()
allow-empty-head.diff (text/x-diff, 1.1 KB)
Handle a "head" keyword with no revision number.

According to rcsfile(5), this is allowed.  In this case, don't invoke
any callbacks.

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_admin_head): Handle
  the case that the "head" keyword doesn't list a revision.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:00:35.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:02:10.000000000 +0100
@@ -91,10 +91,16 @@
   stream_class = None   # subclasses need to define this
 
   def _parse_admin_head(self, token):
-    semi, rev = self.ts.mget(2)
-    self.sink.set_head_revision(rev)
-    if semi != ';':
-      raise RCSExpected(semi, ';')
+    rev = self.ts.get()
+    if rev == ';':
+      # The head revision is not specified.  Just drop the semicolon
+      # on the floor.
+      pass
+    else:
+      self.sink.set_head_revision(rev)
+      semi = self.ts.get()
+      if semi != ';':
+        raise RCSExpected(semi, ';')
 
   def _parse_admin_branch(self, token):
     semi, branch = self.ts.mget(2)
allow-empty-tree.diff (text/x-diff, 2.2 KB)
Handle files with empty "delta" (a.k.a., "tree") part.

According to rcsfile(5), there don't have to be any deltas in an RCS
file.  These files can be created using "rcs -i -t- foofile".  This
change allows rcsparse to handle such files.

This change might help fix cvs2svn Issue #80:
http://cvs2svn.tigris.org/issues/show_bug.cgi?id=80

* lib/vclib/ccvs/rcsparse/common.py (_Parser.admin_token_map): Add an
  entry for "desc" (because if the tree is empty, the next thing
  encountered after the administrative information is "desc").

  (_Parser.parse_rcs_admin): Terminate loop if "desc" is seen.

* lib/vclib/ccvs/rcsparse/test-data/empty-file,v,
  lib/vclib/ccvs/rcsparse/test-data/empty-file.out: Add a test RCS
  file with no contents to test data, and the results expected when
  parsing the file.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:02:10.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:03:46.000000000 +0100
@@ -167,6 +167,7 @@
       'strict' : _parse_admin_strict,
       'comment' : _parse_admin_comment,
       'expand' : _parse_admin_expand,
+      'desc' : None,
       }
 
   def parse_rcs_admin(self):
@@ -186,7 +187,11 @@
           # warn("Unexpected RCS token: $token\n")
           pass
       else:
-        f(self, token)
+        if f is None:
+          self.ts.unget(token)
+          return
+        else:
+          f(self, token)
 
   def parse_rcs_tree(self):
     while 1:
Index: lib/vclib/ccvs/rcsparse/test-data/empty-file,v
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/test-data/empty-file,v	2007-11-10 16:03:46.000000000 +0100
@@ -0,0 +1,10 @@
+head	;
+access;
+symbols;
+locks; strict;
+comment	@# @;
+
+
+
+desc
+@@
Index: lib/vclib/ccvs/rcsparse/test-data/empty-file.out
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/vclib/ccvs/rcsparse/test-data/empty-file.out	2007-11-10 16:04:28.000000000 +0100
@@ -0,0 +1,6 @@
+set_locking('strict')
+set_comment('# ')
+admin_completed()
+tree_completed()
+set_description('')
+parse_completed()
simplify-match.diff (text/x-diff, 1.2 KB)
Simplify the logic in texttools._mxTokenStream.match() (remove duplicate code).

* lib/vclib/ccvs/rcsparse/texttools.py (_mxTokenStream.match):
  Remove duplicate code.
Index: lib/vclib/ccvs/rcsparse/texttools.py
===================================================================
--- lib/vclib/ccvs/rcsparse/texttools.py.orig	2007-11-10 14:14:43.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/texttools.py	2007-11-10 14:15:12.000000000 +0100
@@ -323,16 +323,13 @@
   def match(self, match):
     if self.tokens:
       token = self.tokens.pop()
-      if token != match:
-        raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
-                             'Expected token: %s, but saw: %s'
-                             % (match, token))
     else:
       token = self.get()
-      if token != match:
-        raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
-                             'Expected token: %s, but saw: %s'
-                             % (match, token))
+
+    if token != match:
+      raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
+                           'Expected token: %s, but saw: %s'
+                           % (match, token))
 
   def unget(self, token):
     self.tokens.append(token)
better-rcs-expected.diff (text/x-diff, 1.1 KB)
Improve the error message for RCSExpected exceptions.

* lib/vclib/ccvs/rcsparse/common.py (RCSExpected.__init__): Improve
  the error message passed to the base class constructor.  Also
  improve spacing around the exception class definitions.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 14:46:28.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 14:47:46.000000000 +0100
@@ -72,16 +72,26 @@
 
 class RCSParseError(Exception):
   pass
+
+
 class RCSIllegalCharacter(RCSParseError):
   pass
-### need more work on this one
+
+
 class RCSExpected(RCSParseError):
   def __init__(self, got, wanted):
-    RCSParseError.__init__(self, got, wanted)
+    RCSParseError.__init__(
+        self,
+        'Unexpected parsing error in RCS file.\n'
+        'Expected token: %s, but saw: %s'
+        % (wanted, got)
+        )
+
 
 class RCSStopParser(Exception):
   pass
 
+
 # --------------------------------------------------------------------------
 #
 # STANDARD TOKEN STREAM-BASED PARSER
parse-raises-rcs-expected.diff (text/x-diff, 1.6 KB)
Change the token stream match() methods to raise RCSExpected exceptions.

Previously, they raised a naked RuntimeError.  Nobody seems to catch
these exceptions specifically, anyway.

* lib/vclib/ccvs/rcsparse/default.py (_TokenStream.match): Raise
  RCSExpected exception in the case of a mismatch.

* lib/vclib/ccvs/rcsparse/texttools.py (_mxTokenStream.match): Raise
  RCSExpected exception in the case of a mismatch.
Index: lib/vclib/ccvs/rcsparse/default.py
===================================================================
--- lib/vclib/ccvs/rcsparse/default.py.orig	2007-11-10 14:14:28.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/default.py	2007-11-10 14:15:17.000000000 +0100
@@ -134,8 +134,7 @@
 
     token = self.get()
     if token != match:
-      raise RuntimeError, ('Unexpected parsing error in RCS file.\n' +
-                           'Expected token: %s, but saw: %s' % (match, token))
+      raise common.RCSExpected(token, match)
 
   def unget(self, token):
     "Put this token back, for the next get() to return."
Index: lib/vclib/ccvs/rcsparse/texttools.py
===================================================================
--- lib/vclib/ccvs/rcsparse/texttools.py.orig	2007-11-10 14:15:21.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/texttools.py	2007-11-10 14:15:43.000000000 +0100
@@ -327,9 +327,7 @@
       token = self.get()
 
     if token != match:
-      raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
-                           'Expected token: %s, but saw: %s'
-                           % (match, token))
+      raise common.RCSExpected(token, match)
 
   def unget(self, token):
     self.tokens.append(token)
simplify-semicolon-handling.diff (text/x-diff, 1.7 KB)
Use match() to handle semicolons when possible.

This simplifies the code without a measureable performance cost.

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_admin_head,
  _Parser._parse_admin_branch, _Parser._parse_admin_comment,
  _Parser._parse_admin_expand): Use the token scanner's match() method
  to handle semicolons.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:32:48.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:42:40.000000000 +0100
@@ -108,19 +108,13 @@
       pass
     else:
       self.sink.set_head_revision(rev)
-      semi = self.ts.get()
-      if semi != ';':
-        raise RCSExpected(semi, ';')
+      self.ts.match(';')
 
   def _parse_admin_branch(self, token):
-    semi, branch = self.ts.mget(2)
-    if semi == ';':
+    branch = self.ts.get()
+    if branch != ';':
       self.sink.set_principal_branch(branch)
-    else:
-      if branch == ';':
-        self.ts.unget(semi);
-      else:
-        raise RCSExpected(semi, ';')
+      self.ts.match(';')
 
   def _parse_admin_access(self, token):
     accessors = []
@@ -157,16 +151,13 @@
     self.ts.match(';')
 
   def _parse_admin_comment(self, token):
-    semi, comment = self.ts.mget(2)
-    self.sink.set_comment(comment)
-    if semi != ';':
-      raise RCSExpected(semi, ';')
+    self.sink.set_comment(self.ts.get())
+    self.ts.match(';')
 
   def _parse_admin_expand(self, token):
-    semi, expand_mode = self.ts.mget(2)
+    expand_mode = self.ts.get()
     self.sink.set_expansion(expand_mode)
-    if semi != ';':
-      raise RCSExpected(semi, ';')
+    self.ts.match(';')
 
   admin_token_map = {
       'head' : _parse_admin_head,
rename-locals.diff (text/x-diff, 1.4 KB)
Rename some local variables according to their uses.

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_admin_access,
  _Parser._parse_admin_symbols, _Parser._parse_admin_locks): Rename
  local variables.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 17:03:46.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 17:07:14.000000000 +0100
@@ -119,30 +119,28 @@
   def _parse_admin_access(self, token):
     accessors = []
     while 1:
-      tag = self.ts.get()
-      if tag == ';':
+      accessor = self.ts.get()
+      if accessor == ';':
         if accessors != []:
           self.sink.set_access(accessors)
         return
-      accessors = accessors + [ tag ]
+      accessors = accessors + [ accessor ]
 
   def _parse_admin_symbols(self, token):
     while 1:
-      tag = self.ts.get()
-      if tag == ';':
+      tag_name = self.ts.get()
+      if tag_name == ';':
         break
       self.ts.match(':')
-      tag_name = tag
       tag_rev = self.ts.get()
       self.sink.define_tag(tag_name, tag_rev)
 
   def _parse_admin_locks(self, token):
     while 1:
-      tag = self.ts.get()
-      if tag == ';':
+      locker = self.ts.get()
+      if locker == ';':
         break
       self.ts.match(':')
-      locker = tag
       rev = self.ts.get()
       self.sink.set_locker(rev, locker)
simplify-parse-admin-access.diff (text/x-diff, 838 B)
Simplify _Parser._parse_admin_access().

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_admin_access):
  Simplify logic and use list.append(x) instead of "list = list +
  [x]".
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:47:18.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:47:23.000000000 +0100
@@ -121,10 +121,11 @@
     while 1:
       accessor = self.ts.get()
       if accessor == ';':
-        if accessors != []:
-          self.sink.set_access(accessors)
-        return
-      accessors = accessors + [ accessor ]
+        break
+      accessors.append(accessor)
+
+    if accessors:
+      self.sink.set_access(accessors)
 
   def _parse_admin_symbols(self, token):
     while 1:
parse-rcs-tree-entry.diff (text/x-diff, 5.6 KB)
Extract method _Parser._parse_rcs_tree_entry().

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_rcs_tree_entry):
  New method, extracted from parse_rcs_tree().

  (_Parser.parse_rcs_tree): Use new method.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:50:23.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:50:26.000000000 +0100
@@ -193,6 +193,88 @@
         else:
           f(self, token)
 
+  def _parse_rcs_tree_entry(self, revision):
+    # Parse date
+    semi, date, sym = self.ts.mget(3)
+    if sym != 'date':
+      raise RCSExpected(sym, 'date')
+    if semi != ';':
+      raise RCSExpected(semi, ';')
+
+    # Convert date into timestamp
+    date_fields = string.split(date, '.') + ['0', '0', '0']
+    date_fields = map(string.atoi, date_fields)
+    # need to make the date four digits for timegm
+    EPOCH = 1970
+    if date_fields[0] < EPOCH:
+        if date_fields[0] < 70:
+            date_fields[0] = date_fields[0] + 2000
+        else:
+            date_fields[0] = date_fields[0] + 1900
+        if date_fields[0] < EPOCH:
+            raise ValueError, 'invalid year'
+
+    timestamp = calendar.timegm(tuple(date_fields))
+
+    # Parse author
+    ### NOTE: authors containing whitespace are violations of the
+    ### RCS specification.  We are making an allowance here because
+    ### CVSNT is known to produce these sorts of authors.
+    self.ts.match('author')
+    author = ''
+    while 1:
+      token = self.ts.get()
+      if token == ';':
+        break
+      author = author + token + ' '
+    author = author[:-1]   # toss the trailing space
+
+    # Parse state
+    self.ts.match('state')
+    state = ''
+    while 1:
+      token = self.ts.get()
+      if token == ';':
+        break
+      state = state + token + ' '
+    state = state[:-1]   # toss the trailing space
+
+    # Parse branches
+    self.ts.match('branches')
+    branches = [ ]
+    while 1:
+      token = self.ts.get()
+      if token == ';':
+        break
+      branches.append(token)
+
+    # Parse revision of next delta in chain
+    next, sym = self.ts.mget(2)
+    if sym != 'next':
+      raise RCSExpected(sym, 'next')
+    if next == ';':
+      next = None
+    else:
+      self.ts.match(';')
+
+    # there are some files with extra tags in them. for example:
+    #    owner	640;
+    #    group	15;
+    #    permissions	644;
+    #    hardlinks	@configure.in@;
+    # this is "newphrase" in RCSFILE(5). we just want to skip over these.
+    while 1:
+      token = self.ts.get()
+      if token == 'desc' or token[0] in string.digits:
+        self.ts.unget(token)
+        break
+      # consume everything up to the semicolon
+      while self.ts.get() != ';':
+        pass
+
+    self.sink.define_revision(revision, timestamp, author, state, branches,
+                              next)
+
   def parse_rcs_tree(self):
     while 1:
       revision = self.ts.get()
@@ -202,86 +284,7 @@
         self.ts.unget(revision)
         return
 
-      # Parse date
-      semi, date, sym = self.ts.mget(3)
-      if sym != 'date':
-        raise RCSExpected(sym, 'date')
-      if semi != ';':
-        raise RCSExpected(semi, ';')
-
-      # Convert date into timestamp
-      date_fields = string.split(date, '.') + ['0', '0', '0']
-      date_fields = map(string.atoi, date_fields)
-      # need to make the date four digits for timegm
-      EPOCH = 1970
-      if date_fields[0] < EPOCH:
-          if date_fields[0] < 70:
-              date_fields[0] = date_fields[0] + 2000
-          else:
-              date_fields[0] = date_fields[0] + 1900
-          if date_fields[0] < EPOCH:
-              raise ValueError, 'invalid year'
-
-      timestamp = calendar.timegm(tuple(date_fields))
-
-      # Parse author
-      ### NOTE: authors containing whitespace are violations of the
-      ### RCS specification.  We are making an allowance here because
-      ### CVSNT is known to produce these sorts of authors.
-      self.ts.match('author')
-      author = ''
-      while 1:
-        token = self.ts.get()
-        if token == ';':
-          break
-        author = author + token + ' '
-      author = author[:-1]   # toss the trailing space
-
-      # Parse state
-      self.ts.match('state')
-      state = ''
-      while 1:
-        token = self.ts.get()
-        if token == ';':
-          break
-        state = state + token + ' '
-      state = state[:-1]   # toss the trailing space
-
-      # Parse branches
-      self.ts.match('branches')
-      branches = [ ]
-      while 1:
-        token = self.ts.get()
-        if token == ';':
-          break
-        branches.append(token)
-
-      # Parse revision of next delta in chain
-      next, sym = self.ts.mget(2)
-      if sym != 'next':
-        raise RCSExpected(sym, 'next')
-      if next == ';':
-        next = None
-      else:
-        self.ts.match(';')
-
-      # there are some files with extra tags in them. for example:
-      #    owner	640;
-      #    group	15;
-      #    permissions	644;
-      #    hardlinks	@configure.in@;
-      # this is "newphrase" in RCSFILE(5). we just want to skip over these.
-      while 1:
-        token = self.ts.get()
-        if token == 'desc' or token[0] in string.digits:
-          self.ts.unget(token)
-          break
-        # consume everything up to the semicolon
-        while self.ts.get() != ';':
-          pass
-
-      self.sink.define_revision(revision, timestamp, author, state, branches,
-                                next)
+      self._parse_rcs_tree_entry(revision)
 
   def parse_rcs_description(self):
     self.ts.match('desc')
simplify-tree-entry-parsing.diff (text/x-diff, 1.1 KB)
Simplify _Parser._parse_rcs_tree_entry().

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_rcs_tree_entry):
  Simplify logic.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:51:23.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:52:38.000000000 +0100
@@ -195,11 +195,9 @@
 
   def _parse_rcs_tree_entry(self, revision):
     # Parse date
-    semi, date, sym = self.ts.mget(3)
-    if sym != 'date':
-      raise RCSExpected(sym, 'date')
-    if semi != ';':
-      raise RCSExpected(semi, ';')
+    self.ts.match('date')
+    date = self.ts.get()
+    self.ts.match(';')
 
     # Convert date into timestamp
     date_fields = string.split(date, '.') + ['0', '0', '0']
@@ -249,9 +247,8 @@
       branches.append(token)
 
     # Parse revision of next delta in chain
-    next, sym = self.ts.mget(2)
-    if sym != 'next':
-      raise RCSExpected(sym, 'next')
+    self.ts.match('next')
+    next = self.ts.get()
     if next == ';':
       next = None
     else:
simplify-date-processing.diff (text/x-diff, 1.5 KB)
Improve the handling of dates in _Parser._parse_rcs_tree_entry().

* lib/vclib/ccvs/rcsparse/common.py (_Parser._parse_rcs_tree_entry):
  Adjust the handling of dates to conform more strictly to
  rcsparse(5), and simplify slightly.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:50:32.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 16:50:34.000000000 +0100
@@ -200,19 +200,17 @@
     self.ts.match(';')
 
     # Convert date into timestamp
-    date_fields = string.split(date, '.') + ['0', '0', '0']
+    date_fields = string.split(date, '.')
+    # According to rcsfile(5): the year "contains just the last two
+    # digits of the year for years from 1900 through 1999, and all the
+    # digits of years thereafter".
+    if len(date_fields[0]) == 2:
+      date_fields[0] = '19' + date_fields[0]
     date_fields = map(string.atoi, date_fields)
-    # need to make the date four digits for timegm
     EPOCH = 1970
     if date_fields[0] < EPOCH:
-        if date_fields[0] < 70:
-            date_fields[0] = date_fields[0] + 2000
-        else:
-            date_fields[0] = date_fields[0] + 1900
-        if date_fields[0] < EPOCH:
-            raise ValueError, 'invalid year'
-
-    timestamp = calendar.timegm(tuple(date_fields))
+      raise ValueError, 'invalid year'
+    timestamp = calendar.timegm(tuple(date_fields + [0, 0, 0]))
 
     # Parse author
     ### NOTE: authors containing whitespace are violations of the
read-until-semicolon.diff (text/x-diff, 2.4 KB)
Add method _Parser._read_until_semicolon(), and use it in other methods.

* lib/vclib/ccvs/rcsparse/common.py (_Parser._read_until_semicolon):
  New method.

  (_Parser._parse_admin_access, _Parser._parse_rcs_tree_entry): Use
  the new method.
Index: lib/vclib/ccvs/rcsparse/common.py
===================================================================
--- lib/vclib/ccvs/rcsparse/common.py.orig	2007-11-10 16:55:30.000000000 +0100
+++ lib/vclib/ccvs/rcsparse/common.py	2007-11-10 17:00:13.000000000 +0100
@@ -100,6 +100,22 @@
 class _Parser:
   stream_class = None   # subclasses need to define this
 
+  def _read_until_semicolon(self):
+    """Read all tokens up to (but not including) the next semicolon token.
+
+    Return the tokens as a list.  Consume the terminating
+    semicolon."""
+
+    tokens = []
+
+    while 1:
+      token = self.ts.get()
+      if token == ';':
+        break
+      tokens.append(token)
+
+    return tokens
+
   def _parse_admin_head(self, token):
     rev = self.ts.get()
     if rev == ';':
@@ -117,13 +133,7 @@
       self.ts.match(';')
 
   def _parse_admin_access(self, token):
-    accessors = []
-    while 1:
-      accessor = self.ts.get()
-      if accessor == ';':
-        break
-      accessors.append(accessor)
-
+    accessors = self._read_until_semicolon()
     if accessors:
       self.sink.set_access(accessors)
 
@@ -217,13 +227,7 @@
     ### RCS specification.  We are making an allowance here because
     ### CVSNT is known to produce these sorts of authors.
     self.ts.match('author')
-    author = ''
-    while 1:
-      token = self.ts.get()
-      if token == ';':
-        break
-      author = author + token + ' '
-    author = author[:-1]   # toss the trailing space
+    author = ' '.join(self._read_until_semicolon())
 
     # Parse state
     self.ts.match('state')
@@ -237,12 +241,7 @@
 
     # Parse branches
     self.ts.match('branches')
-    branches = [ ]
-    while 1:
-      token = self.ts.get()
-      if token == ';':
-        break
-      branches.append(token)
+    branches = self._read_until_semicolon()
 
     # Parse revision of next delta in chain
     self.ts.match('next')
@@ -264,8 +263,7 @@
         self.ts.unget(token)
         break
       # consume everything up to the semicolon
-      while self.ts.get() != ';':
-        pass
+      self._read_until_semicolon()
 
     self.sink.define_revision(revision, timestamp, author, state, branches,
                               next)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.