[PATCH] Built-in cvsps for hg cvsimport

Frank Kingswood frank at kingswood-consulting.co.uk
Thu Apr 3 16:45:40 CDT 2008


# HG changeset patch
# User Frank Kingswood <frank at kingswood-consulting.co.uk>
# Date 1207258957 -3600
# Node ID 236da9d3c0131a62f0ce397512e7d46f32bbebe8
# Parent  6c4e12682fb985137f7e68dbaa9ee9444b1281b0
Built-in cvsps for hg cvsimport.

This patch adds a built-in cvsps program.
To use this, set these options in .hgrc.

        [convert]
        cvsps = builtin

The built-in cvsps uses cvs rlog on the repository (it does not do
direct cvs server calls, it runs the cvs executable), sorts the commit
log messages, and merges commits with identical messages, author and
branch name and a date within the 60-seconds fuzz window.

This builtin cvsps code has been found to work succesfully in cases
where the traditional external cvsps program generates incorrect
changesets.

The builtin cvsps code has an important additional feature: it can
close CVS branches based on magic tags in the CVS log message.

When a log message includes the text {{mergefrombranch X}} then cvsps
will set a second parent for a changeset. When a log message includes
the text {{mergetobranch X}} then cvsps will insert a dummy changeset
(with no members, so no changes) merging the branch into the named
branch (which must exist).

For convenience, the cvsps.py script can also be run as a standalone
replacement for cvsps, as long as the mercurial modules are in the
PYTHONPATH. When run standalone, cvsps.py accepts these cvsps options
-b -p -r --root -v and -z.

diff -r 6c4e12682fb9 -r 236da9d3c013 hgext/convert/convcmd.py
--- a/hgext/convert/convcmd.py	Thu Apr 03 13:47:05 2008 +0200
+++ b/hgext/convert/convcmd.py	Thu Apr 03 22:42:37 2008 +0100
@@ -290,7 +290,7 @@
                 # convert log message to local encoding without using
                 # tolocal() because util._encoding conver() use it as
                 # 'utf-8'
-                self.ui.status("%d %s\n" % (num, recode(desc)))
+                self.ui.status(util.ellipsis("%d %s\n" % (num, recode(desc)),80))
                 self.ui.note(_("source: %s\n" % recode(c)))
                 self.copy(c)
 
diff -r 6c4e12682fb9 -r 236da9d3c013 hgext/convert/cvs.py
--- a/hgext/convert/cvs.py	Thu Apr 03 13:47:05 2008 +0200
+++ b/hgext/convert/cvs.py	Thu Apr 03 22:42:37 2008 +0100
@@ -3,8 +3,10 @@
 import os, locale, re, socket
 from cStringIO import StringIO
 from mercurial import util
+from mercurial.i18n import _
 
 from common import NoRepo, commit, converter_source, checktool
+from cvsps import cvsps_create_log,cvsps_create_changeset
 
 class convert_cvs(converter_source):
     def __init__(self, ui, path, rev=None):
@@ -14,10 +16,12 @@
         if not os.path.exists(cvs):
             raise NoRepo("%s does not look like a CVS checkout" % path)
 
+        checktool('cvs')
         self.cmd = ui.config('convert', 'cvsps', 'cvsps -A -u --cvs-direct -q')
         cvspsexe = self.cmd.split(None, 1)[0]
-        for tool in (cvspsexe, 'cvs'):
-            checktool(tool)
+        self.builtin = cvspsexe=='builtin'
+        if not self.builtin:
+            checktool(cvspsexe)
 
         self.changeset = {}
         self.files = {}
@@ -28,10 +32,11 @@
         self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
         self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
         self.encoding = locale.getpreferredencoding()
-        self._parse()
+
+        self._parse(ui)
         self._connect()
 
-    def _parse(self):
+    def _parse(self,ui):
         if self.changeset:
             return
 
@@ -56,80 +61,108 @@
             id = None
             state = 0
             filerevids = {}
-            for l in util.popen(cmd):
-                if state == 0: # header
-                    if l.startswith("PatchSet"):
-                        id = l[9:-2]
-                        if maxrev and int(id) > maxrev:
-                            # ignore everything
-                            state = 3
-                    elif l.startswith("Date"):
-                        date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
-                        date = util.datestr(date)
-                    elif l.startswith("Branch"):
-                        branch = l[8:-1]
-                        self.parent[id] = self.lastbranch.get(branch, 'bad')
-                        self.lastbranch[branch] = id
-                    elif l.startswith("Ancestor branch"):
-                        ancestor = l[17:-1]
-                        # figure out the parent later
-                        self.parent[id] = self.lastbranch[ancestor]
-                    elif l.startswith("Author"):
-                        author = self.recode(l[8:-1])
-                    elif l.startswith("Tag:") or l.startswith("Tags:"):
-                        t = l[l.index(':')+1:]
-                        t = [ut.strip() for ut in t.split(',')]
-                        if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
-                            self.tags.update(dict.fromkeys(t, id))
-                    elif l.startswith("Log:"):
-                        # switch to gathering log
-                        state = 1
-                        log = ""
-                elif state == 1: # log
-                    if l == "Members: \n":
-                        # switch to gathering members
-                        files = {}
-                        oldrevs = []
-                        log = self.recode(log[:-1])
-                        state = 2
-                    else:
-                        # gather log
-                        log += l
-                elif state == 2: # members
-                    if l == "\n": # start of next entry
-                        state = 0
-                        p = [self.parent[id]]
-                        if id == "1":
-                            p = []
-                        if branch == "HEAD":
-                            branch = ""
-                        if branch:
-                            latest = None
-                            # the last changeset that contains a base
-                            # file is our parent
-                            for r in oldrevs:
-                                latest = max(filerevids.get(r, None), latest)
-                            if latest:
-                                p = [latest]
 
-                        # add current commit to set
-                        c = commit(author=author, date=date, parents=p,
-                                   desc=log, branch=branch)
-                        self.changeset[id] = c
-                        self.files[id] = files
-                    else:
-                        colon = l.rfind(':')
-                        file = l[1:colon]
-                        rev = l[colon+1:-2]
-                        oldrev, rev = rev.split("->")
-                        files[file] = rev
+            if self.builtin:
+                # builtin cvsps code
+                ui.status(_('using builtin cvsps\n'))
 
-                        # save some information for identifying branch points
-                        oldrevs.append("%s:%s" % (oldrev, file))
-                        filerevids["%s:%s" % (rev, file)] = id
-                elif state == 3:
-                    # swallow all input
-                    continue
+                for cs in cvsps_create_changeset(cvsps_create_log([None],ui),ui):
+                    if maxrev and cs.Id>maxrev:
+                        break
+                    id = str(cs.Id)
+
+                    cs.Author = self.recode(cs.Author)
+                    self.lastbranch[cs.Branch] = id
+                    cs.Comment = self.recode(cs.Comment)
+                    date = util.datestr(cs.Date)
+                    self.tags.update(dict.fromkeys(cs.Tags,id))
+
+                    files = {}
+                    for f in cs.Entries:
+                        files[f.File]="%s%s"%('.'.join([str(x) for x in f.Revision]),['','(DEAD)'][f.Dead])
+
+                    # add current commit to set
+                    c=commit(author=cs.Author,date=date,
+                             parents=[str(p.Id) for p in cs.Parents],
+                             desc=cs.Comment,branch=cs.Branch or '')
+                    self.changeset[id]=c
+                    self.files[id]=files
+            else:
+                # external cvsps
+                for l in util.popen(cmd):
+                    if state == 0: # header
+                        if l.startswith("PatchSet"):
+                            id = l[9:-2]
+                            if maxrev and int(id) > maxrev:
+                                # ignore everything
+                                state = 3
+                        elif l.startswith("Date"):
+                            date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
+                            date = util.datestr(date)
+                        elif l.startswith("Branch"):
+                            branch = l[8:-1]
+                            self.parent[id] = self.lastbranch.get(branch, 'bad')
+                            self.lastbranch[branch] = id
+                        elif l.startswith("Ancestor branch"):
+                            ancestor = l[17:-1]
+                            # figure out the parent later
+                            self.parent[id] = self.lastbranch[ancestor]
+                        elif l.startswith("Author"):
+                            author = self.recode(l[8:-1])
+                        elif l.startswith("Tag:") or l.startswith("Tags:"):
+                            t = l[l.index(':')+1:]
+                            t = [ut.strip() for ut in t.split(',')]
+                            if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
+                                self.tags.update(dict.fromkeys(t, id))
+                        elif l.startswith("Log:"):
+                            # switch to gathering log
+                            state = 1
+                            log = ""
+                    elif state == 1: # log
+                        if l == "Members: \n":
+                            # switch to gathering members
+                            files = {}
+                            oldrevs = []
+                            log = self.recode(log[:-1])
+                            state = 2
+                        else:
+                            # gather log
+                            log += l
+                    elif state == 2: # members
+                        if l == "\n": # start of next entry
+                            state = 0
+                            p = [self.parent[id]]
+                            if id == "1":
+                                p = []
+                            if branch == "HEAD":
+                                branch = ""
+                            if branch:
+                                latest = None
+                                # the last changeset that contains a base
+                                # file is our parent
+                                for r in oldrevs:
+                                    latest = max(filerevids.get(r, None), latest)
+                                if latest:
+                                    p = [latest]
+
+                            # add current commit to set
+                            c = commit(author=author, date=date, parents=p,
+                                       desc=log, branch=branch)
+                            self.changeset[id] = c
+                            self.files[id] = files
+                        else:
+                            colon = l.rfind(':')
+                            file = l[1:colon]
+                            rev = l[colon+1:-2]
+                            oldrev, rev = rev.split("->")
+                            files[file] = rev
+
+                            # save some information for identifying branch points
+                            oldrevs.append("%s:%s" % (oldrev, file))
+                            filerevids["%s:%s" % (rev, file)] = id
+                    elif state == 3:
+                        # swallow all input
+                        continue
 
             self.heads = self.lastbranch.values()
         finally:
diff -r 6c4e12682fb9 -r 236da9d3c013 hgext/convert/cvsps.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/convert/cvsps.py	Thu Apr 03 22:42:37 2008 +0100
@@ -0,0 +1,612 @@
+#!/usr/bin/env python
+#
+# Mercurial built-in replacement for cvsps.
+#
+# Copyright 2008, Frank Kingswood <frank at kingswood-consulting.co.uk>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+import os
+import re
+import sys
+from mercurial import util
+from mercurial.i18n import _
+
+class cvsps_log_entry:
+   '''Class cvsps_log_entry has the following attributes:
+      .Author    - author name as CVS knows it
+      .Branch    - name of branch this revision is on
+      .Branches  - revision tuple of branches starting at this revision
+      .Comment   - commit message
+      .Date      - the commit date as a (time,tz) tuple
+      .Dead      - true if file revision is dead
+      .File      - Name of file
+      .Lines     - a tuple (+lines,-lines) or None
+      .Parent    - Previous revision of this entry
+      .RCS       - name of file as returned from CVS
+      .Revision  - revision number as tuple
+      .Tags      - list of tags on the file
+   '''
+   def __init__(self,**entries):
+      self.__dict__.update(entries)
+
+class cvsps_log_error(Exception):
+   pass
+
+def cvsps_create_log(dirs,ui,root=None,rlog=True):
+   '''Collect the CVS rlog'''
+
+   # reusing strings typically saves about 40% of memory
+   _cache={}
+   def cache(s):
+      try:
+         return _cache[s]
+      except:
+         _cache[s]=s
+      return s
+
+   ui.status(_('collecting CVS rlog\n'))
+
+   log=[]      # list of cvsps_log_entry objects containing the CVS state
+
+   # patterns to match in CVS (r)log output, by state of use
+   re_00=re.compile('RCS file: (.+)$')
+   re_01=re.compile('cvs \\[r?log aborted\\]: (.+)$')
+   re_02=re.compile('cvs (r?log|server): (.+)\n$')
+   re_03=re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
+   re_10=re.compile('Working file: (.+)$')
+   re_20=re.compile('symbolic names:')
+   re_30=re.compile('\t(.+): ([\\d.]+)$')
+   re_31=re.compile('----------------------------$')
+   re_32=re.compile('=============================================================================$')
+   re_50=re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
+   re_60=re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?')
+   re_70=re.compile('branches: (.+);$')
+
+   for d in dirs:
+
+      prefix=''   # leading path to strip of what we get from CVS
+
+      if d is None:
+         # Current working directory
+
+         # Get the real directory in the repository
+         try:
+            prefix=d=file('CVS/Repository').read().strip()
+         except IOError:
+            raise cvsps_log_error()
+               
+         if not prefix.endswith('/'):
+            prefix+='/'
+
+         # Use the Root file in the sandbox, if it exists
+         try:
+            r=file('CVS/Root').read().strip()
+         except IOError:
+            r=root
+
+      else:
+         # user specified a directory in the repository
+         r=root
+
+      if not r:
+         r=os.environ.get("CVSROOT",None)
+
+      # build the CVS commandline
+      cmd=['cvs','-q']
+      if r:
+         cmd.append('-d%s'%r)
+         p=r.split(':')[-1]
+         if not p.endswith('/'):
+            p+='/'
+         prefix=p+prefix
+      cmd.append(['log','rlog'][rlog])
+      cmd.append(d)
+
+      # state machine begins here
+      tags={}     # dictionary of revisions on current file with their tags
+      state=0
+      store=False # set when a new record can be appended
+
+      cmd=[util.shellquote(arg) for arg in cmd]
+      cmd=util.quotecommand(' '.join(cmd))
+
+      for line in os.popen(cmd):
+         if line.endswith('\n'):
+            line=line[:-1]
+         #ui.status('state=%d line=%r\n'%(state,line))
+
+         if state==0:
+            match=re_00.match(line)
+            if match:
+               rcs=match.group(1)
+               tags={}
+               if rlog:
+                  filename=rcs[:-2]
+                  if filename.startswith(prefix):
+                     filename=filename[len(prefix):]
+                  if filename.startswith('/'):
+                     filename=filename[1:]
+                  filename=filename.replace('/Attic/','/')
+                  state=2
+                  continue
+               state=1
+               continue
+            match=re_01.match(line)
+            if match:
+               raise Exception(match.group(1))
+            match=re_02.match(line)
+            if match:
+               raise Exception(match.group(2))
+            if re_03.match(line):
+               raise Exception(line)
+
+         elif state==1:
+            match=re_10.match(line)
+            assert match,_('RCS file must always be followed by Working file')
+            filename=match.group(1)
+            state=2
+
+         elif state==2:
+            if re_20.match(line):
+               state=3
+
+         elif state==3:
+            match=re_30.match(line)
+            if match:
+               rev=[int(x) for x in match.group(2).split('.')]
+
+               # Convert magic branch number to an odd-numbered one
+               revn=len(rev)
+               if revn>3 and (revn%2)==0 and rev[-2]==0:
+                  rev=rev[:-2]+rev[-1:]
+               rev=tuple(rev)
+
+               if rev not in tags:
+                  tags[rev]=[]
+               tags[rev].append(match.group(1))
+
+            elif re_31.match(line):
+               state=5
+            elif re_32.match(line):
+               state=0
+
+         elif state==4:
+            if re_31.match(line):
+               state=5
+            else:
+               assert not re_32.match(line),_('Must have at least some revisions')
+
+         elif state==5:
+            match=re_50.match(line)
+            assert match,_('expected revision number')
+            e=cvsps_log_entry(RCS=cache(rcs),File=cache(filename),Revision=tuple([int(x) for x in match.group(1).split('.')]),Branches=[],Parent=None)
+            state=6
+
+         elif state==6:
+            match=re_60.match(line)
+            assert match,_('revision must be followed by date line')
+            d=match.group(1)
+            if d[2]=='/':
+               # Y2K
+               d='19'+d
+
+            if len(d.split())!=3:
+               d=d+" UTC"
+            e.Date=util.parsedate(d,['%y/%m/%d %H:%M:%S','%Y/%m/%d %H:%M:%S','%Y-%m-%d %H:%M:%S'])
+            e.Author=cache(match.group(2))
+            e.Dead=match.group(3).lower()=='dead'
+
+            if match.group(5):
+               if match.group(6):
+                  e.Lines=(int(match.group(5)),int(match.group(6)))
+               else:
+                  e.Lines=(int(match.group(5)),0)
+            elif match.group(6):
+               e.Lines=(0,int(match.group(6)))
+            else:
+               e.Lines=None
+            e.Comment=[]
+            state=7
+
+         elif state==7:
+            m=re_70.match(line)
+            if m:
+               e.Branches=[tuple([int(y) for y in x.strip().split('.')]) for x in m.group(1).split(';')]
+               state=8
+            elif re_31.match(line):
+               state=5
+               store=True
+            elif re_32.match(line):
+               state=0
+               store=True
+            else:
+               e.Comment.append(line)
+
+         elif state==8:
+            if re_31.match(line):
+               state=5
+               store=True
+            elif re_32.match(line):
+               state=0
+               store=True
+            else:
+               e.Comment.append(line)
+
+         if store:
+            store=False
+            e.Tags=[cache(x) for x in tags.get(e.Revision,[])]
+            e.Tags.sort()
+            e.Comment=cache('\n'.join(e.Comment))
+
+            revn=len(e.Revision)
+            if revn>3 and (revn%2)==0:
+               e.Branch=tags.get(e.Revision[:-1],[None])[0]
+            else:
+               e.Branch=None
+
+            log.append(e)
+
+            if len(log)%100==0:
+               ui.status(util.ellipsis('%d %s'%(len(log),e.File),80)+'\n')
+
+   log.sort(key=lambda x:(x.RCS,x.Revision))
+
+   # find parent revisions
+   versions={}
+   for e in log:
+      branch=e.Revision[:-1]
+
+      p=versions.get((e.RCS,branch),None)
+      if p is None:
+         p=e.Revision[:-2]
+      e.Parent=p
+      versions[(e.RCS,branch)]=e.Revision
+
+   ui.status(_('%d log entries\n')%len(log))
+
+   return log
+
+
+class cvsps_changeset:
+   '''Class cvsps_changeset has the following attributes:
+      .Author    - author name as CVS knows it
+      .Branch    - name of branch this changeset is on, or None
+      .Comment   - commit message
+      .Date      - the commit date as a (time,tz) tuple
+      .Entries   - list of cvsps_log_entry objects in this changeset
+      .Parent    - list of one or two parent changesets
+      .Tags      - list of tags on this changeset
+   '''
+   def __init__(self,**entries):
+      self.__dict__.update(entries)
+
+
+def cvsps_create_changeset(log,ui,fuzz=60):
+   '''Convert log into changesets.'''
+
+   ui.status(_('creating changesets\n'))
+
+   # Merge changesets
+
+   log.sort(key=lambda x:(x.Comment,x.Author,x.Branch,x.Date))
+
+   changeset=[]
+   files={}
+   c=None
+   for i,e in enumerate(log):
+
+      # Check if log entry belongs to the current changeset or not.
+      if not (c and
+              e.Comment==c.Comment and
+              e.Author==c.Author and
+              e.Branch==c.Branch and
+              (c.Date[0]+c.Date[1])<=(e.Date[0]+e.Date[1])<=(c.Date[0]+c.Date[1])+fuzz and
+              e.File not in files):
+         c=cvsps_changeset(Comment=e.Comment,Author=e.Author,
+                           Branch=e.Branch,Date=e.Date,Entries=[])
+         changeset.append(c)
+         files={}
+         if len(changeset)%100==0:
+            ui.status(util.ellipsis('%d %s'%(len(changeset),repr(e.Comment)[1:-1]),80)+'\n')
+
+      e.Changeset=c
+      c.Entries.append(e)
+      files[e.File]=True
+      c.Date=e.Date       # changeset date is date of latest commit in it
+
+   # Sort files in each changeset
+
+   for c in changeset:
+      def pathcompare(l,r):
+         'Mimic cvsps sorting order'
+         l=l.split('/')
+         r=r.split('/')
+         nl=len(l)
+         nr=len(r)
+         n=min(nl,nr)
+         for i in range(n):
+            if i+1==nl and nl<nr:
+               return -1
+            elif i+1==nr and nl>nr:
+               return +1
+            elif l[i]<r[i]:
+               return -1
+            elif l[i]>r[i]:
+               return +1
+         return 0
+      def entitycompare(l,r):
+         return pathcompare(l.File,r.File)
+
+      c.Entries.sort(cmp=entitycompare)
+
+   # Sort changesets by date
+
+   def cscmp(l,r):
+      d=sum(l.Date)-sum(r.Date)
+      if d:
+         return d
+
+      # detect vendor branches and initial commits on a branch
+      le={}
+      for e in l.Entries:
+         le[e.RCS]=e.Revision
+      re={}
+      for e in r.Entries:
+         re[e.RCS]=e.Revision
+
+      d=0
+      for e in l.Entries:
+         if re.get(e.RCS,None)==e.Parent:
+            assert not d
+            d=1
+            break
+
+      for e in r.Entries:
+         if le.get(e.RCS,None)==e.Parent:
+            assert not d
+            d=-1
+            break
+
+      return d
+
+   changeset.sort(cmp=cscmp)
+
+   # Collect tags
+   
+   globaltags={}
+   for c in changeset:
+      tags={}
+      for e in c.Entries:
+         for tag in e.Tags:
+            # remember which is the latest changeset to have this tag
+            globaltags[tag]=c
+
+   for c in changeset:
+      tags={}
+      for e in c.Entries:
+         for tag in e.Tags:
+            tags[tag]=True
+      # remember tags only if this is the latest changeset to have it
+      tagnames=[tag for tag in tags if globaltags[tag] is c]
+      tagnames.sort()
+      c.Tags=tagnames
+
+   # Find parent changesets, handle {{mergetobranch BRANCHNAME}} 
+   # by inserting dummy changesets with two parents, and handle
+   # {{mergefrom BRANCHNAME}} by setting two parents.
+   
+   versions={}
+   for i,c in enumerate(changeset):
+      for f in c.Entries:
+         versions[(f.RCS,f.Revision)]=i
+
+   re_mergeto=re.compile(r'{{mergetobranch (\w+)}}')
+   re_mergefrom=re.compile(r'{{mergefrombranch (\w+)}}')
+
+   branches={}
+   n=len(changeset)
+   i=0
+   while i<n:
+      c=changeset[i]
+
+      p=None
+      if c.Branch in branches:
+         p=branches[c.Branch]
+      else:
+         for f in c.Entries:
+            p=max(p,versions.get((f.RCS,f.Parent),None))
+
+      c.Parents=[]
+      if p is not None:
+         c.Parents.append(changeset[p])
+
+      m=re_mergefrom.search(c.Comment)
+      if m:
+         m=m.group(1)
+         if m=='HEAD':
+            m=None
+         if m in branches and c.Branch!=m:
+            c.Parents.append(changeset[branches[m]])
+
+      m=re_mergeto.search(c.Comment)
+      if m:
+         m=m.group(1)
+         if m=='HEAD':
+            m=None
+         if m in branches and c.Branch!=m:
+            # insert empty changeset for merge
+            cc=cvsps_changeset(Comment="convert-repo: CVS merge from branch %s"%c.Branch,
+                               Author=c.Author,Branch=m,Date=c.Date,
+                               Entries=[],Tags=[],
+                               Parents=[changeset[branches[m]],c])
+            changeset.insert(i+1,cc)
+            branches[m]=i+1
+
+            # close source branch
+            if c.Branch in branches:
+               del branches[c.Branch]
+
+            # adjust our loop counters now we have inserted a new entry
+            n+=1
+            i+=2
+            continue
+
+      branches[c.Branch]=i
+      i+=1
+
+   # Number changesets
+
+   for i,c in enumerate(changeset):
+      c.Id=i+1
+
+   ui.status(_('%d changeset entries\n')%len(changeset))
+
+   return changeset
+
+
+def main():
+   '''Main program to mimic cvsps.'''
+   import cPickle as pickle
+   import os
+   from optparse import OptionParser,SUPPRESS_HELP
+
+   op=OptionParser(usage='%prog [-brpvzcClL] path',
+                   description='Read CVS rlog for current directory or named '
+                               'path in repository, and convert the log to changesets '
+                               'based on matching commit log entries and dates.')
+
+   # Options that are ignored for compatibility with cvsps
+   op.add_option('-A',dest='Ignore',action='store_true',help=SUPPRESS_HELP)
+   op.add_option('--cvs-direct',dest='Ignore',action='store_true',help=SUPPRESS_HELP)
+   op.add_option('-q',dest='Ignore',action='store_true',help=SUPPRESS_HELP)
+   op.add_option('-u',dest='Ignore',action='store_true',help=SUPPRESS_HELP)
+   op.add_option('-x',dest='Ignore',action='store_true',help=SUPPRESS_HELP)
+
+   # Main options
+   op.add_option('-b',dest='Branches',action='append',default=[],
+                 help='Only return changes on specified branches')
+   op.add_option('-r',dest='Revisions',action='append',default=[],
+                 help='Only return changes after or between specified tags')
+   op.add_option('-p',dest='Prefix',action='store',default='',
+                 help='Prefix to remove from file names')
+   op.add_option('-v',dest='Verbose',action='count',default=0,
+                 help='Be verbose')
+   op.add_option('-z',dest='Fuzz',action='store',type='int',default=60,
+                 help='Set commit time fuzz',metavar='seconds')
+   op.add_option('--root',dest='Root',action='store',
+                 help='Specify cvsroot',metavar='cvsroot')
+
+   # Options specific to this version
+   op.add_option('--parents',dest='Parents',action='store_true',
+                 help='Show parent changesets')
+   op.add_option('--read-cs',dest='ReadChangeset',action='store',default='',
+                 help='Read changeset database',metavar='file')
+   op.add_option('--write-cs',dest='WriteChangeset',action='store',default='',
+                 help='Write changeset database',metavar='file')
+   op.add_option('--read-log',dest='ReadLog',action='store',default='',
+                 help='Read log database',metavar='file')
+   op.add_option('--write-log',dest='WriteLog',action='store',default='',
+                 help='Write log database',metavar='file')
+   op.add_option('--debug',dest='Debug',action='store_true',help=SUPPRESS_HELP)
+
+   options,args=op.parse_args()
+
+   # Create a ui object for printing progress messages
+   class UI:
+      def __init__(self,verbose):
+         if not verbose:
+            self.status=self.nostatus
+      def status(self,msg):
+         sys.stderr.write(msg)
+      def nostatus(self,msg):
+         pass
+   ui=UI(options.Verbose)
+
+   # Create changesets or read from pickle file
+   if options.ReadChangeset:
+      changeset=pickle.load(file(options.ReadChangeset))
+   else:
+
+      # Create log or read from pickle file
+      if options.ReadLog:
+         log=pickle.load(file(options.ReadLog))
+      else:
+         try:
+            log=cvsps_create_log(args or [None],ui,root=options.Root)
+         except cvsps_log_error:
+            print "Not a CVS sandbox."
+            return
+
+         if options.WriteLog:
+            pickle.dump(log,file(options.WriteLog,'w'))
+
+      changeset=cvsps_create_changeset(log,ui,options.Fuzz)
+      del log
+
+      if options.WriteChangeset:
+         pickle.dump(changeset,file(options.WriteChangeset,'w'))
+
+   # Print changesets (optionally filtered)
+
+   off=len(options.Revisions)
+   for cs in changeset:
+
+      # limit by branches
+      if options.Branches and (cs.Branch or 'HEAD') not in options.Branches:
+         continue
+
+      # limit by tags
+      if options.Revisions and off:
+         if str(cs.Id)==options.Revisions[0]:
+            off=False
+         for t in cs.Tags:
+            if t==options.Revisions[0]:
+               off=False
+               break
+
+      if not off:
+         # Note: trailing spaces on several lines here are needed to have
+         #       bug-for-bug compatibility with cvsps.
+         print '---------------------'
+         print 'PatchSet %d '%cs.Id
+         print 'Date: %s'%util.datestr(cs.Date,'%Y/%m/%d %H:%M:%S')
+         print 'Author: %s'%cs.Author
+         print 'Branch: %s'%(cs.Branch or 'HEAD')
+         print 'Tag%s: %s '%(['','s'][len(cs.Tags)>1],
+                             ','.join(cs.Tags) or '(none)')
+         if options.Parents and cs.Parents:
+            if len(cs.Parents)>1:
+               print 'Parents: %s'%(','.join([str(p.Id) for p in cs.Parents]))
+            else:
+               print 'Parent: %d'%cs.Parents[0].Id
+
+         print 'Log:'
+         print cs.Comment
+         print
+         print 'Members: '
+         for f in cs.Entries:
+            fn=f.File
+            if fn.startswith(options.Prefix):
+               fn=fn[len(options.Prefix):]
+            print '\t%s:%s->%s%s '%(fn,'.'.join([str(x) for x in f.Parent]) or 'INITIAL',
+                                    '.'.join([str(x) for x in f.Revision]),['','(DEAD)'][f.Dead])
+         print
+
+      if len(options.Revisions)>1 and not off:
+         # see if we reached the end tag
+         if str(cs.Id)==options.Revisions[1]:
+            off=True
+         for t in cs.Tags:
+            if t==options.Revisions[1]:
+               off=True
+               break
+         if off:
+            break
+
+
+if __name__=='__main__':
+   main()
+
+# EOF cvsps.py


More information about the Mercurial-devel mailing list