monotone.py
410 lines
| 13.5 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 | |||
|
r28372 | import os | ||
import re | ||||
|
r29205 | from mercurial.i18n import _ | ||
|
r43355 | from mercurial.pycompat import open | ||
|
r28372 | from mercurial import ( | ||
error, | ||||
|
r36347 | pycompat, | ||
|
r28372 | ) | ||
|
r36625 | from mercurial.utils import dateutil | ||
|
r6306 | |||
|
r28372 | from . import common | ||
|
r43346 | |||
|
r28372 | class monotone_source(common.converter_source, common.commandline): | ||
|
r35168 | def __init__(self, ui, repotype, path=None, revs=None): | ||
common.converter_source.__init__(self, ui, repotype, path, revs) | ||||
|
r25748 | if revs and len(revs) > 1: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'monotone source does not support specifying ' | ||
b'multiple revs' | ||||
|
r43346 | ) | ||
) | ||||
|
r43347 | common.commandline.__init__(self, ui, b'mtn') | ||
|
r6307 | |||
|
r6306 | self.ui = ui | ||
self.path = path | ||||
|
r13760 | self.automatestdio = False | ||
|
r25748 | self.revs = revs | ||
|
r6306 | |||
|
r43346 | norepo = common.NoRepo( | ||
|
r43347 | _(b"%s does not look like a monotone repository") % path | ||
|
r43346 | ) | ||
|
r43347 | if not os.path.exists(os.path.join(path, b'_MTN')): | ||
|
r8052 | # Could be a monotone repository (SQLite db file) | ||
try: | ||||
|
r43347 | f = open(path, b'rb') | ||
|
r14179 | header = f.read(16) | ||
f.close() | ||||
|
r16688 | except IOError: | ||
|
r43347 | header = b'' | ||
if header != b'SQLite format 3\x00': | ||||
|
r8052 | raise norepo | ||
|
r7973 | |||
|
r6306 | # regular expressions for parsing monotone output | ||
|
r43346 | space = br'\s*' | ||
name = br'\s+"((?:\\"|[^"])*)"\s*' | ||||
value = name | ||||
|
r36411 | revision = br'\s+\[(\w+)\]\s*' | ||
|
r43346 | lines = br'(?:.|\n)+' | ||
|
r6307 | |||
|
r43347 | self.dir_re = re.compile(space + b"dir" + name) | ||
self.file_re = re.compile( | ||||
space + b"file" + name + b"content" + revision | ||||
) | ||||
|
r43346 | self.add_file_re = re.compile( | ||
|
r43347 | space + b"add_file" + name + b"content" + revision | ||
|
r43346 | ) | ||
self.patch_re = re.compile( | ||||
|
r43347 | space + b"patch" + name + b"from" + revision + b"to" + revision | ||
|
r43346 | ) | ||
|
r43347 | self.rename_re = re.compile(space + b"rename" + name + b"to" + name) | ||
self.delete_re = re.compile(space + b"delete" + name) | ||||
self.tag_re = re.compile(space + b"tag" + name + b"revision" + revision) | ||||
|
r43346 | self.cert_re = re.compile( | ||
|
r43347 | lines + space + b"name" + name + b"value" + value | ||
|
r43346 | ) | ||
|
r6306 | |||
|
r43347 | attr = space + b"file" + lines + space + b"attr" + space | ||
|
r43346 | self.attr_execute_re = re.compile( | ||
|
r43347 | attr + b'"mtn:execute"' + space + b'"true"' | ||
|
r43346 | ) | ||
|
r6306 | |||
# cached data | ||||
self.manifest_rev = None | ||||
self.manifest = None | ||||
|
r6307 | self.files = None | ||
|
r43346 | self.dirs = None | ||
|
r6307 | |||
|
r43347 | common.checktool(b'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): | ||||
|
r43906 | kwargs['d'] = self.path | ||
|
r43347 | return self.run0(b'automate', *args, **kwargs) | ||
|
r6307 | |||
|
r13760 | def mtnrunstdio(self, *args, **kwargs): | ||
# Prepare the command in automate stdio format | ||||
|
r36347 | kwargs = pycompat.byteskwargs(kwargs) | ||
|
r13760 | command = [] | ||
|
r43375 | for k, v in pycompat.iteritems(kwargs): | ||
|
r43347 | command.append(b"%d:%s" % (len(k), k)) | ||
|
r13760 | if v: | ||
|
r43347 | command.append(b"%d:%s" % (len(v), v)) | ||
|
r13760 | if command: | ||
|
r43347 | command.insert(0, b'o') | ||
command.append(b'e') | ||||
|
r13760 | |||
|
r43347 | command.append(b'l') | ||
|
r13760 | for arg in args: | ||
|
r43347 | command.append(b"%d:%s" % (len(arg), arg)) | ||
command.append(b'e') | ||||
command = b''.join(command) | ||||
|
r13760 | |||
|
r43347 | self.ui.debug(b"mtn: sending '%s'\n" % command) | ||
|
r13760 | self.mtnwritefp.write(command) | ||
self.mtnwritefp.flush() | ||||
return self.mtnstdioreadcommandoutput(command) | ||||
def mtnstdioreadpacket(self): | ||||
read = None | ||||
|
r43347 | commandnbr = b'' | ||
while read != b':': | ||||
|
r13760 | read = self.mtnreadfp.read(1) | ||
if not read: | ||||
|
r43347 | raise error.Abort(_(b'bad mtn packet - no end of commandnbr')) | ||
|
r13760 | commandnbr += read | ||
commandnbr = commandnbr[:-1] | ||||
stream = self.mtnreadfp.read(1) | ||||
|
r43347 | if stream not in b'mewptl': | ||
raise error.Abort( | ||||
_(b'bad mtn packet - bad stream type %s') % stream | ||||
) | ||||
|
r13760 | |||
read = self.mtnreadfp.read(1) | ||||
|
r43347 | if read != b':': | ||
raise error.Abort(_(b'bad mtn packet - no divider before size')) | ||||
|
r13760 | |||
read = None | ||||
|
r43347 | lengthstr = b'' | ||
while read != b':': | ||||
|
r13760 | read = self.mtnreadfp.read(1) | ||
if not read: | ||||
|
r43347 | raise error.Abort(_(b'bad mtn packet - no end of packet size')) | ||
|
r13760 | lengthstr += read | ||
try: | ||||
|
r41666 | length = pycompat.long(lengthstr[:-1]) | ||
|
r13760 | except TypeError: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'bad mtn packet - bad packet size %s') % lengthstr | ||
|
r43346 | ) | ||
|
r13760 | |||
read = self.mtnreadfp.read(length) | ||||
if len(read) != length: | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b"bad mtn packet - unable to read full packet " | ||
b"read %s of %s" | ||||
|
r43346 | ) | ||
% (len(read), length) | ||||
) | ||||
|
r13760 | |||
return (commandnbr, stream, length, read) | ||||
def mtnstdioreadcommandoutput(self, command): | ||||
|
r13792 | retval = [] | ||
|
r13760 | while True: | ||
commandnbr, stream, length, output = self.mtnstdioreadpacket() | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b'mtn: read packet %s:%s:%d\n' % (commandnbr, stream, length) | ||
|
r43346 | ) | ||
|
r13760 | |||
|
r43347 | if stream == b'l': | ||
|
r13760 | # End of command | ||
|
r43347 | if output != b'0': | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"mtn command '%s' returned %s") % (command, output) | ||
|
r43346 | ) | ||
|
r13760 | break | ||
|
r43347 | elif stream in b'ew': | ||
|
r13760 | # Error, warning output | ||
|
r43347 | self.ui.warn(_(b'%s error:\n') % self.command) | ||
|
r13760 | self.ui.warn(output) | ||
|
r43347 | elif stream == b'p': | ||
|
r13760 | # Progress messages | ||
|
r43347 | self.ui.debug(b'mtn: ' + output) | ||
elif stream == b'm': | ||||
|
r13760 | # Main stream - command output | ||
|
r13792 | retval.append(output) | ||
|
r13760 | |||
|
r43347 | return b''.join(retval) | ||
|
r13760 | |||
|
r6306 | def mtnloadmanifest(self, rev): | ||
if self.manifest_rev == rev: | ||||
return | ||||
|
r43347 | self.manifest = self.mtnrun(b"get_manifest_of", rev).split(b"\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: | ||
|
r43347 | attr = b"" | ||
|
r6306 | name = m.group(1) | ||
node = m.group(2) | ||||
if self.attr_execute_re.match(e): | ||||
|
r43347 | attr += b"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): | ||
|
r43346 | certs = { | ||
|
r43347 | b"author": b"<missing>", | ||
b"date": b"<missing>", | ||||
b"changelog": b"<missing>", | ||||
b"branch": b"<missing>", | ||||
|
r43346 | } | ||
|
r43347 | certlist = self.mtnrun(b"certs", rev) | ||
|
r9823 | # mtn < 0.45: | ||
# key "test@selenic.com" | ||||
# mtn >= 0.45: | ||||
# key [ff58a7ffb771907c4ff68995eada1c4da068d328] | ||||
|
r44472 | certlist = re.split(br'\n\n {6}key ["\[]', certlist) | ||
|
r9823 | for e in certlist: | ||
|
r6306 | m = self.cert_re.match(e) | ||
if m: | ||||
|
r6632 | name, value = m.groups() | ||
|
r43347 | value = value.replace(br'\"', b'"') | ||
value = value.replace(br'\\', b'\\') | ||||
|
r6632 | certs[name] = value | ||
|
r8101 | # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306 | ||
|
r8125 | # and all times are stored in UTC | ||
|
r43347 | certs[b"date"] = certs[b"date"].split(b'.')[0] + b" UTC" | ||
|
r6306 | return certs | ||
# implement the converter_source interface: | ||||
|
r6307 | |||
|
r6306 | def getheads(self): | ||
|
r25748 | if not self.revs: | ||
|
r43347 | return self.mtnrun(b"leaves").splitlines() | ||
|
r6306 | else: | ||
|
r25748 | return self.revs | ||
|
r6306 | |||
|
r22300 | def getchanges(self, rev, full): | ||
if full: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"convert from monotone does not support --full") | ||
|
r43346 | ) | ||
|
r43347 | revision = self.mtnrun(b"get_revision", rev).split(b"\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 | ||
|
r43347 | if tofile.startswith(todir + b'/'): | ||
|
r43346 | 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(): | ||
|
r42431 | self.ui.debug( | ||
|
r43347 | b"copying file in renamed directory from '%s' to '%s'" | ||
|
r43346 | % (fromfile, tofile), | ||
|
r43347 | b'\n', | ||
|
r43346 | ) | ||
|
r8099 | 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: | ||
|
r43347 | data = self.mtnrun(b"get_file_of", name, r=rev) | ||
|
r16689 | except Exception: | ||
|
r22296 | return None, None | ||
|
r11134 | self.mtnloadmanifest(rev) | ||
|
r43347 | node, attr = self.files.get(name, (None, b"")) | ||
|
r11134 | return data, attr | ||
|
r6307 | |||
def getcommit(self, rev): | ||||
|
r13779 | extra = {} | ||
certs = self.mtngetcerts(rev) | ||||
|
r43347 | if certs.get(b'suspend') == certs[b"branch"]: | ||
|
r45652 | extra[b'close'] = b'1' | ||
|
r43347 | dateformat = b"%Y-%m-%dT%H:%M:%S" | ||
|
r28372 | return common.commit( | ||
|
r43347 | author=certs[b"author"], | ||
date=dateutil.datestr(dateutil.strdate(certs[b"date"], dateformat)), | ||||
desc=certs[b"changelog"], | ||||
|
r6306 | rev=rev, | ||
|
r43347 | parents=self.mtnrun(b"parents", rev).splitlines(), | ||
branch=certs[b"branch"], | ||||
|
r43346 | extra=extra, | ||
) | ||||
|
r6306 | |||
def gettags(self): | ||||
tags = {} | ||||
|
r43347 | for e in self.mtnrun(b"tags").split(b"\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 | ||||
try: | ||||
|
r43347 | versionstr = self.mtnrunsingle(b"interface_version") | ||
|
r13760 | version = float(versionstr) | ||
except Exception: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"unable to determine mtn automate interface version") | ||
|
r43346 | ) | ||
|
r13760 | |||
if version >= 12.0: | ||||
self.automatestdio = True | ||||
|
r43346 | self.ui.debug( | ||
|
r43347 | b"mtn automate version %f - using automate stdio\n" % version | ||
|
r43346 | ) | ||
|
r13760 | |||
# launch the long-running automate stdio process | ||||
|
r43346 | self.mtnwritefp, self.mtnreadfp = self._run2( | ||
|
r43347 | b'automate', b'stdio', b'-d', self.path | ||
|
r43346 | ) | ||
|
r13760 | # read the headers | ||
read = self.mtnreadfp.readline() | ||||
|
r43347 | if read != b'format-version: 2\n': | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'mtn automate stdio header unexpected: %s') % read | ||
|
r43346 | ) | ||
|
r43347 | while read != b'\n': | ||
|
r13760 | read = self.mtnreadfp.readline() | ||
if not read: | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b"failed to reach end of mtn automate " | ||
b"stdio headers" | ||||
|
r43346 | ) | ||
) | ||||
|
r13760 | else: | ||
|
r43346 | self.ui.debug( | ||
|
r43347 | b"mtn automate version %s - not using automate stdio " | ||
b"(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version | ||||
|
r43346 | ) | ||
|
r13760 | |||
def after(self): | ||||
if self.automatestdio: | ||||
self.mtnwritefp.close() | ||||
self.mtnwritefp = None | ||||
self.mtnreadfp.close() | ||||
self.mtnreadfp = None | ||||