--- 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()