patch: monotone source for mercurial convert extension

Mikkel Fahnøe Jørgensen mikkel at dvide.com
Sat Feb 2 12:39:58 CST 2008


Here is an export from monotone to mercurial.

It is based on the mecurial convert extension in hgext/convert.
It's my first Python program, so ...

Testing is limited to my own monotone repository, which failed
miserably when converted with Tailor.

Named branches are supported.

File and directory renames are handled (although I'm quite sure if
both to and from filenames should be in the changelist of a changeset
- for me it works best as is - although hg serve web interface
occasionally links to non-existing files in a rename changeset.

Filemap option in the convert tool is not supported.
Monotone symbolic links (new feature I think) not supported nor investigated.

An obvious optimization would be to support mtn automate stdio so
monotone can run in a single process during the export.

There might be issues with setting the correct locale.

Tested with monotone automation interface v. 6.0, monotone version
0.38 on OS-X 10.4 Intel and current mercurial source tip.

examples:

hg convert mydb.mtn mynewdb.hg
hg convert --authors myauthormappingfile --datesort mydb.mtn mynewdb.hg
hg convert --debug mydb.mtn mynewdb.hg

If this patch is accepted for mercurial, I wouldn't mind if a monotone
core developer took over maintainance, since the automation interface
seems to change occasionally.


Regards,

Mikkel


# HG changeset patch
# User Mikkel Fahnøe Jørgensen <mikkel at dvide.com>
# Date 1201976099 -3600
# Node ID 464355ba0325e617bb91e0f9d193a006c3dac312
# Parent  9f1e6ab76069641a5e3ff5b0831da0a3f83365cb
initial version of monotone source for convert extension

diff -r 9f1e6ab76069 -r 464355ba0325 hgext/convert/__init__.py
--- a/hgext/convert/__init__.py	Thu Jan 31 14:44:19 2008 -0600
+++ b/hgext/convert/__init__.py	Sat Feb 02 19:14:59 2008 +0100
@@ -19,6 +19,7 @@ def convert(ui, src, dest=None, revmapfi
     - Darcs
     - git
     - Subversion
+    - Monotone

     Accepted destination formats:
     - Mercurial
diff -r 9f1e6ab76069 -r 464355ba0325 hgext/convert/convcmd.py
--- a/hgext/convert/convcmd.py	Thu Jan 31 14:44:19 2008 -0600
+++ b/hgext/convert/convcmd.py	Sat Feb 02 19:14:59 2008 +0100
@@ -11,6 +11,7 @@ from git import convert_git
 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
 import filemap

 import os, shutil
@@ -23,6 +24,7 @@ source_converters = [
     ('svn', svn_source),
     ('hg', mercurial_source),
     ('darcs', darcs_source),
+    ('mtn', monotone_source),
     ]

 sink_converters = [
diff -r 9f1e6ab76069 -r 464355ba0325 hgext/convert/monotone.py
--- /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()



More information about the Mercurial-devel mailing list