monotone.py
365 lines
| 12.9 KiB
| text/x-python
|
PythonLexer
|
r8250 | # monotone.py - monotone support for the convert extension | ||
# | ||||
# Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and | ||||
# others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r6306 | |||
|
r7873 | import os, re | ||
|
r26587 | from mercurial import util, error | ||
|
r7873 | from common import NoRepo, commit, converter_source, checktool | ||
|
r6332 | from common import commandline | ||
|
r6307 | from mercurial.i18n import _ | ||
|
r6306 | |||
|
r6307 | class monotone_source(converter_source, commandline): | ||
|
r25748 | def __init__(self, ui, path=None, revs=None): | ||
converter_source.__init__(self, ui, path, revs) | ||||
if revs and len(revs) > 1: | ||||
|
r26587 | raise error.Abort(_('monotone source does not support specifying ' | ||
|
r25748 | 'multiple revs')) | ||
|
r6307 | commandline.__init__(self, ui, 'mtn') | ||
|
r6306 | self.ui = ui | ||
self.path = path | ||||
|
r13760 | self.automatestdio = False | ||
|
r25748 | self.revs = revs | ||
|
r6306 | |||
|
r10938 | norepo = NoRepo(_("%s does not look like a monotone repository") | ||
% path) | ||||
|
r7973 | if not os.path.exists(os.path.join(path, '_MTN')): | ||
|
r8052 | # Could be a monotone repository (SQLite db file) | ||
try: | ||||
|
r14179 | f = file(path, 'rb') | ||
header = f.read(16) | ||||
f.close() | ||||
|
r16688 | except IOError: | ||
|
r8052 | header = '' | ||
if header != 'SQLite format 3\x00': | ||||
raise norepo | ||||
|
r7973 | |||
|
r6306 | # regular expressions for parsing monotone output | ||
space = r'\s*' | ||||
|
r6632 | name = r'\s+"((?:\\"|[^"])*)"\s*' | ||
|
r6306 | value = name | ||
revision = r'\s+\[(\w+)\]\s*' | ||||
lines = r'(?:.|\n)+' | ||||
|
r6307 | |||
self.dir_re = re.compile(space + "dir" + name) | ||||
|
r10282 | 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) | ||||
|
r6307 | self.rename_re = re.compile(space + "rename" + name + "to" + name) | ||
|
r6376 | self.delete_re = re.compile(space + "delete" + name) | ||
|
r10282 | self.tag_re = re.compile(space + "tag" + name + "revision" + | ||
revision) | ||||
self.cert_re = re.compile(lines + space + "name" + name + | ||||
"value" + value) | ||||
|
r6306 | |||
attr = space + "file" + lines + space + "attr" + space | ||||
|
r10282 | self.attr_execute_re = re.compile(attr + '"mtn:execute"' + | ||
space + '"true"') | ||||
|
r6306 | |||
# cached data | ||||
self.manifest_rev = None | ||||
self.manifest = None | ||||
|
r6307 | self.files = None | ||
self.dirs = None | ||||
|
r6332 | checktool('mtn', abort=False) | ||
|
r6307 | |||
def mtnrun(self, *args, **kwargs): | ||||
|
r13760 | if self.automatestdio: | ||
return self.mtnrunstdio(*args, **kwargs) | ||||
else: | ||||
return self.mtnrunsingle(*args, **kwargs) | ||||
def mtnrunsingle(self, *args, **kwargs): | ||||
|
r6307 | kwargs['d'] = self.path | ||
return self.run0('automate', *args, **kwargs) | ||||
|
r13760 | def mtnrunstdio(self, *args, **kwargs): | ||
# Prepare the command in automate stdio format | ||||
command = [] | ||||
for k, v in kwargs.iteritems(): | ||||
command.append("%s:%s" % (len(k), k)) | ||||
if v: | ||||
command.append("%s:%s" % (len(v), v)) | ||||
if command: | ||||
command.insert(0, 'o') | ||||
command.append('e') | ||||
command.append('l') | ||||
for arg in args: | ||||
command += "%s:%s" % (len(arg), arg) | ||||
command.append('e') | ||||
command = ''.join(command) | ||||
self.ui.debug("mtn: sending '%s'\n" % command) | ||||
self.mtnwritefp.write(command) | ||||
self.mtnwritefp.flush() | ||||
return self.mtnstdioreadcommandoutput(command) | ||||
def mtnstdioreadpacket(self): | ||||
read = None | ||||
commandnbr = '' | ||||
while read != ':': | ||||
read = self.mtnreadfp.read(1) | ||||
if not read: | ||||
|
r26587 | raise error.Abort(_('bad mtn packet - no end of commandnbr')) | ||
|
r13760 | commandnbr += read | ||
commandnbr = commandnbr[:-1] | ||||
stream = self.mtnreadfp.read(1) | ||||
if stream not in 'mewptl': | ||||
|
r26587 | raise error.Abort(_('bad mtn packet - bad stream type %s') % stream) | ||
|
r13760 | |||
read = self.mtnreadfp.read(1) | ||||
if read != ':': | ||||
|
r26587 | raise error.Abort(_('bad mtn packet - no divider before size')) | ||
|
r13760 | |||
read = None | ||||
lengthstr = '' | ||||
while read != ':': | ||||
read = self.mtnreadfp.read(1) | ||||
if not read: | ||||
|
r26587 | raise error.Abort(_('bad mtn packet - no end of packet size')) | ||
|
r13760 | lengthstr += read | ||
try: | ||||
length = long(lengthstr[:-1]) | ||||
except TypeError: | ||||
|
r26587 | raise error.Abort(_('bad mtn packet - bad packet size %s') | ||
|
r13760 | % lengthstr) | ||
read = self.mtnreadfp.read(length) | ||||
if len(read) != length: | ||||
|
r26587 | raise error.Abort(_("bad mtn packet - unable to read full packet " | ||
|
r13760 | "read %s of %s") % (len(read), length)) | ||
return (commandnbr, stream, length, read) | ||||
def mtnstdioreadcommandoutput(self, command): | ||||
|
r13792 | retval = [] | ||
|
r13760 | while True: | ||
commandnbr, stream, length, output = self.mtnstdioreadpacket() | ||||
self.ui.debug('mtn: read packet %s:%s:%s\n' % | ||||
(commandnbr, stream, length)) | ||||
if stream == 'l': | ||||
# End of command | ||||
if output != '0': | ||||
|
r26587 | raise error.Abort(_("mtn command '%s' returned %s") % | ||
|
r13760 | (command, output)) | ||
break | ||||
elif stream in 'ew': | ||||
# Error, warning output | ||||
self.ui.warn(_('%s error:\n') % self.command) | ||||
self.ui.warn(output) | ||||
elif stream == 'p': | ||||
# Progress messages | ||||
self.ui.debug('mtn: ' + output) | ||||
elif stream == 'm': | ||||
# Main stream - command output | ||||
|
r13792 | retval.append(output) | ||
|
r13760 | |||
|
r13792 | return ''.join(retval) | ||
|
r13760 | |||
|
r6306 | def mtnloadmanifest(self, rev): | ||
if self.manifest_rev == rev: | ||||
return | ||||
|
r6307 | self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n") | ||
|
r6306 | self.manifest_rev = rev | ||
|
r6307 | self.files = {} | ||
self.dirs = {} | ||||
|
r6306 | |||
|
r6307 | for e in self.manifest: | ||
|
r6306 | m = self.file_re.match(e) | ||
|
r6307 | if m: | ||
|
r6306 | attr = "" | ||
name = m.group(1) | ||||
node = m.group(2) | ||||
if self.attr_execute_re.match(e): | ||||
attr += "x" | ||||
|
r6307 | self.files[name] = (node, attr) | ||
|
r6306 | m = self.dir_re.match(e) | ||
if m: | ||||
|
r6307 | self.dirs[m.group(1)] = True | ||
|
r6306 | |||
def mtnisfile(self, name, rev): | ||||
# a non-file could be a directory or a deleted or renamed file | ||||
self.mtnloadmanifest(rev) | ||||
|
r8458 | return name in self.files | ||
|
r6307 | |||
|
r6306 | def mtnisdir(self, name, rev): | ||
self.mtnloadmanifest(rev) | ||||
|
r8458 | return name in self.dirs | ||
|
r6307 | |||
|
r6306 | def mtngetcerts(self, rev): | ||
certs = {"author":"<missing>", "date":"<missing>", | ||||
"changelog":"<missing>", "branch":"<missing>"} | ||||
|
r9823 | certlist = self.mtnrun("certs", rev) | ||
# mtn < 0.45: | ||||
# key "test@selenic.com" | ||||
# mtn >= 0.45: | ||||
# key [ff58a7ffb771907c4ff68995eada1c4da068d328] | ||||
certlist = re.split('\n\n key ["\[]', certlist) | ||||
for e in certlist: | ||||
|
r6306 | m = self.cert_re.match(e) | ||
if m: | ||||
|
r6632 | name, value = m.groups() | ||
value = value.replace(r'\"', '"') | ||||
value = value.replace(r'\\', '\\') | ||||
certs[name] = value | ||||
|
r8101 | # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306 | ||
|
r8125 | # and all times are stored in UTC | ||
certs["date"] = certs["date"].split('.')[0] + " UTC" | ||||
|
r6306 | return certs | ||
# implement the converter_source interface: | ||||
|
r6307 | |||
|
r6306 | def getheads(self): | ||
|
r25748 | if not self.revs: | ||
|
r6307 | return self.mtnrun("leaves").splitlines() | ||
|
r6306 | else: | ||
|
r25748 | return self.revs | ||
|
r6306 | |||
|
r22300 | def getchanges(self, rev, full): | ||
if full: | ||||
|
r26779 | raise error.Abort(_("convert from monotone does not support " | ||
"--full")) | ||||
|
r6307 | revision = self.mtnrun("get_revision", rev).split("\n\n") | ||
|
r6306 | files = {} | ||
|
r8123 | ignoremove = {} | ||
|
r8099 | renameddirs = [] | ||
|
r6306 | copies = {} | ||
for e in revision: | ||||
m = self.add_file_re.match(e) | ||||
if m: | ||||
files[m.group(1)] = rev | ||||
|
r8123 | ignoremove[m.group(1)] = rev | ||
|
r6306 | 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. | ||||
|
r6376 | m = self.delete_re.match(e) | ||
if m: | ||||
files[m.group(1)] = rev | ||||
|
r6306 | m = self.rename_re.match(e) | ||
if m: | ||||
toname = m.group(2) | ||||
fromname = m.group(1) | ||||
if self.mtnisfile(toname, rev): | ||||
|
r8123 | ignoremove[toname] = 1 | ||
|
r6306 | copies[toname] = fromname | ||
files[toname] = rev | ||||
files[fromname] = rev | ||||
|
r8099 | elif self.mtnisdir(toname, rev): | ||
renameddirs.append((fromname, toname)) | ||||
# Directory renames can be handled only once we have recorded | ||||
# all new files | ||||
for fromdir, todir in renameddirs: | ||||
renamed = {} | ||||
for tofile in self.files: | ||||
|
r8123 | if tofile in ignoremove: | ||
|
r8099 | continue | ||
if tofile.startswith(todir + '/'): | ||||
renamed[tofile] = fromdir + tofile[len(todir):] | ||||
|
r8124 | # Avoid chained moves like: | ||
# d1(/a) => d3/d1(/a) | ||||
# d2 => d3 | ||||
ignoremove[tofile] = 1 | ||||
|
r8099 | for tofile, fromfile in renamed.items(): | ||
|
r8100 | self.ui.debug (_("copying file in renamed directory " | ||
"from '%s' to '%s'") | ||||
|
r8099 | % (fromfile, tofile), '\n') | ||
files[tofile] = rev | ||||
copies[tofile] = fromfile | ||||
for fromfile in renamed.values(): | ||||
files[fromfile] = rev | ||||
|
r24395 | return (files.items(), copies, set()) | ||
|
r6306 | |||
def getfile(self, name, rev): | ||||
if not self.mtnisfile(name, rev): | ||||
|
r22296 | return None, None | ||
|
r6307 | try: | ||
|
r11134 | data = self.mtnrun("get_file_of", name, r=rev) | ||
|
r16689 | except Exception: | ||
|
r22296 | return None, None | ||
|
r11134 | self.mtnloadmanifest(rev) | ||
node, attr = self.files.get(name, (None, "")) | ||||
return data, attr | ||||
|
r6307 | |||
def getcommit(self, rev): | ||||
|
r13779 | extra = {} | ||
certs = self.mtngetcerts(rev) | ||||
if certs.get('suspend') == certs["branch"]: | ||||
|
r24178 | extra['close'] = 1 | ||
|
r6306 | return commit( | ||
author=certs["author"], | ||||
date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")), | ||||
desc=certs["changelog"], | ||||
rev=rev, | ||||
|
r6307 | parents=self.mtnrun("parents", rev).splitlines(), | ||
|
r13779 | branch=certs["branch"], | ||
extra=extra) | ||||
|
r6306 | |||
def gettags(self): | ||||
tags = {} | ||||
|
r6307 | for e in self.mtnrun("tags").split("\n\n"): | ||
|
r6306 | 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 | ||||
|
r16687 | raise NotImplementedError | ||
|
r13760 | |||
def before(self): | ||||
# Check if we have a new enough version to use automate stdio | ||||
version = 0.0 | ||||
try: | ||||
versionstr = self.mtnrunsingle("interface_version") | ||||
version = float(versionstr) | ||||
except Exception: | ||||
|
r26587 | raise error.Abort(_("unable to determine mtn automate interface " | ||
|
r13760 | "version")) | ||
if version >= 12.0: | ||||
self.automatestdio = True | ||||
self.ui.debug("mtn automate version %s - using automate stdio\n" % | ||||
version) | ||||
# launch the long-running automate stdio process | ||||
self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio', | ||||
'-d', self.path) | ||||
# read the headers | ||||
read = self.mtnreadfp.readline() | ||||
if read != 'format-version: 2\n': | ||||
|
r26587 | raise error.Abort(_('mtn automate stdio header unexpected: %s') | ||
|
r13760 | % read) | ||
while read != '\n': | ||||
read = self.mtnreadfp.readline() | ||||
if not read: | ||||
|
r26587 | raise error.Abort(_("failed to reach end of mtn automate " | ||
|
r13760 | "stdio headers")) | ||
else: | ||||
self.ui.debug("mtn automate version %s - not using automate stdio " | ||||
"(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version) | ||||
def after(self): | ||||
if self.automatestdio: | ||||
self.mtnwritefp.close() | ||||
self.mtnwritefp = None | ||||
self.mtnreadfp.close() | ||||
self.mtnreadfp = None | ||||