initial version of monotone source for convert extension
authorMikkel Fahnøe Jørgensen <mikkel@dvide.com>
Sat, 02 Feb 2008 19:14:59 +0100
changeset 6306 2f9de4aaea9e
parent 6305 e8d447d91cdb
child 6307 6840668e3bf6
initial version of monotone source for convert extension
hgext/convert/__init__.py
hgext/convert/convcmd.py
hgext/convert/monotone.py
--- a/hgext/convert/__init__.py	Thu Mar 20 16:42:51 2008 +0100
+++ b/hgext/convert/__init__.py	Sat Feb 02 19:14:59 2008 +0100
@@ -19,6 +19,7 @@
     - Darcs
     - git
     - Subversion
+    - Monotone
     - GNU Arch
 
     Accepted destination formats:
--- a/hgext/convert/convcmd.py	Thu Mar 20 16:42:51 2008 +0100
+++ b/hgext/convert/convcmd.py	Sat Feb 02 19:14:59 2008 +0100
@@ -11,6 +11,7 @@
 from git import convert_git
 from hg import mercurial_source, mercurial_sink
 from subversion import debugsvnlog, svn_source, svn_sink
+from monotone import monotone_source
 from gnuarch import gnuarch_source
 import filemap
 
@@ -32,6 +33,7 @@
     ('svn', svn_source),
     ('hg', mercurial_source),
     ('darcs', darcs_source),
+    ('mtn', monotone_source),
     ('gnuarch', gnuarch_source),
     ]
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/convert/monotone.py	Sat Feb 02 19:14:59 2008 +0100
@@ -0,0 +1,214 @@
+# monotone support for the convert extension
+
+import os
+import re
+import time
+from mercurial import util
+
+from common import NoRepo, commit, converter_source, checktool
+
+class monotone_source(converter_source):
+    def __init__(self, ui, path=None, rev=None):
+        converter_source.__init__(self, ui, path, rev)
+        
+        self.ui = ui
+        self.path = path
+
+         
+        # regular expressions for parsing monotone output
+        
+        space    = r'\s*'
+        name     = r'\s+"((?:[^"]|\\")*)"\s*'
+        value    = name
+        revision = r'\s+\[(\w+)\]\s*'
+        lines    = r'(?:.|\n)+'
+        
+        self.dir_re      = re.compile(space + "dir"      + name)
+        self.file_re     = re.compile(space + "file"     + name + "content" + revision)
+        self.add_file_re = re.compile(space + "add_file" + name + "content" + revision)
+        self.patch_re    = re.compile(space + "patch"    + name + "from" + revision + "to" + revision)
+        self.rename_re   = re.compile(space + "rename"   + name + "to" + name)
+        self.tag_re      = re.compile(space + "tag"      + name + "revision" + revision)
+        self.cert_re     = re.compile(lines + space + "name" + name + "value" + value)
+
+        attr = space + "file" + lines + space + "attr" + space
+        self.attr_execute_re = re.compile(attr  + '"mtn:execute"' + space + '"true"')
+
+        # cached data
+        
+        self.manifest_rev = None
+        self.manifest = None
+        self.files = None   
+        self.dirs  = None     
+        
+        norepo = NoRepo("%s does not look like a monotone repo" % path)
+        if not os.path.exists(path):
+            raise norepo
+        
+        checktool('mtn')
+        
+        # test if there are are any revisions
+        self.rev = None
+        try :
+            self.getheads()
+        except :
+            raise norepo        
+
+        self.rev = rev
+
+    
+    def mtncmd(self, arg):
+        cmdline = "mtn -d %s automate %s" % (util.shellquote(self.path), arg)
+        self.ui.debug(cmdline, '\n')
+        p = util.popen(cmdline)
+        result = p.read()
+        if p.close():
+            raise IOError()
+        return result
+    
+    def mtnloadmanifest(self, rev):
+        if self.manifest_rev == rev:
+            return
+        self.manifest_rev = rev
+        self.manifest = self.mtncmd("get_manifest_of %s" % rev).split("\n\n")
+        
+        manifest = self.manifest
+        files = {}
+        dirs = {}
+
+        for e in manifest:
+            m = self.file_re.match(e)
+            if m:                
+                attr = ""
+                name = m.group(1)
+                node = m.group(2)
+                if self.attr_execute_re.match(e):
+                    attr += "x"
+                files[name] = (node, attr)
+            m = self.dir_re.match(e)
+            if m:
+                dirs[m.group(1)] = True
+        
+        self.files = files
+        self.dirs = dirs
+
+    def mtnisfile(self, name, rev):
+        # a non-file could be a directory or a deleted or renamed file
+        self.mtnloadmanifest(rev)
+        try :
+            self.files[name]
+            return True
+        except KeyError:
+            return False
+            
+    def mtnisdir(self, name, rev):
+        self.mtnloadmanifest(rev)
+        try :
+            self.dirs[name]
+            return True
+        except KeyError:
+            return False
+    
+    def mtngetcerts(self, rev):
+        certs = {"author":"<missing>", "date":"<missing>",
+            "changelog":"<missing>", "branch":"<missing>"}
+        cert_list = self.mtncmd("certs %s" % rev).split("\n\n")
+        for e in cert_list:
+            m = self.cert_re.match(e)
+            if m:
+                certs[m.group(1)] = m.group(2)
+        return certs
+        
+    def mtngetparents(self, rev):
+        parents = self.mtncmd("parents %s" % rev).strip("\n").split("\n")
+        p = []
+        for x in parents:
+            if len(x) >= 40: # blank revs have been seen otherwise
+                p.append(x)
+        return p
+
+    def mtnrenamefiles(self, files, fromdir, todir):
+        renamed = {}
+        for tofile in files:
+            suffix = tofile.lstrip(todir)
+            if todir + suffix == tofile:
+                renamed[tofile] = (fromdir + suffix).lstrip("/")
+        return renamed
+
+    
+    # implement the converter_source interface:
+    
+    def getheads(self):
+        if not self.rev or self.rev == "":
+            return self.mtncmd("leaves").splitlines()
+        else:
+            return [self.rev]
+
+    def getchanges(self, rev):
+        revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
+        files = {}
+        copies = {}
+        for e in revision:
+            m = self.add_file_re.match(e)
+            if m:
+                files[m.group(1)] = rev
+            m = self.patch_re.match(e)
+            if m:
+                files[m.group(1)] = rev
+
+            # Delete/rename is handled later when the convert engine
+            # discovers an IOError exception from getfile,
+            # but only if we add the "from" file to the list of changes.
+            m = self.rename_re.match(e)
+            if m:
+                toname = m.group(2)
+                fromname = m.group(1)
+                if self.mtnisfile(toname, rev):
+                    copies[toname] = fromname
+                    files[toname] = rev
+                    files[fromname] = rev
+                if self.mtnisdir(toname, rev):
+                    renamed = self.mtnrenamefiles(self.files, fromname, toname)
+                    for tofile, fromfile in renamed.items():
+                        self.ui.debug (("copying file in renamed dir from '%s' to '%s'" % (fromfile, tofile)), "\n")
+                        files[tofile] = rev
+                    for fromfile in renamed.values():
+                        files[fromfile] = rev
+
+        return (files.items(), copies)
+        
+    def getmode(self, name, rev):
+        self.mtnloadmanifest(rev)
+        try :
+            node, attr = self.files[name]
+            return attr
+        except KeyError:
+            return ""
+        
+    def getfile(self, name, rev):
+        if not self.mtnisfile(name, rev):
+            raise IOError() # file was deleted or renamed
+        return self.mtncmd("get_file_of %s -r %s" % (util.shellquote(name), rev))
+    
+    def getcommit(self, rev):        
+        certs   = self.mtngetcerts(rev)
+        return commit(
+            author=certs["author"],
+            date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
+            desc=certs["changelog"],
+            rev=rev,
+            parents=self.mtngetparents(rev),
+            branch=certs["branch"])
+
+    def gettags(self):
+        tags = {}
+        for e in self.mtncmd("tags").split("\n\n"):
+            m = self.tag_re.match(e)
+            if m:
+                tags[m.group(1)] = m.group(2)
+        return tags
+
+    def getchangedfiles(self, rev, i):
+        # This function is only needed to support --filemap
+        # ... and we don't support that
+        raise NotImplementedError()