1# monotone.py - monotone support for the convert extension 2# 3# Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and 4# others 5# 6# This software may be used and distributed according to the terms of the 7# GNU General Public License version 2 or any later version. 8from __future__ import absolute_import 9 10import os 11import re 12 13from mercurial.i18n import _ 14from mercurial.pycompat import open 15from mercurial import ( 16 error, 17 pycompat, 18) 19from mercurial.utils import dateutil 20 21from . import common 22 23 24class monotone_source(common.converter_source, common.commandline): 25 def __init__(self, ui, repotype, path=None, revs=None): 26 common.converter_source.__init__(self, ui, repotype, path, revs) 27 if revs and len(revs) > 1: 28 raise error.Abort( 29 _( 30 b'monotone source does not support specifying ' 31 b'multiple revs' 32 ) 33 ) 34 common.commandline.__init__(self, ui, b'mtn') 35 36 self.ui = ui 37 self.path = path 38 self.automatestdio = False 39 self.revs = revs 40 41 norepo = common.NoRepo( 42 _(b"%s does not look like a monotone repository") % path 43 ) 44 if not os.path.exists(os.path.join(path, b'_MTN')): 45 # Could be a monotone repository (SQLite db file) 46 try: 47 f = open(path, b'rb') 48 header = f.read(16) 49 f.close() 50 except IOError: 51 header = b'' 52 if header != b'SQLite format 3\x00': 53 raise norepo 54 55 # regular expressions for parsing monotone output 56 space = br'\s*' 57 name = br'\s+"((?:\\"|[^"])*)"\s*' 58 value = name 59 revision = br'\s+\[(\w+)\]\s*' 60 lines = br'(?:.|\n)+' 61 62 self.dir_re = re.compile(space + b"dir" + name) 63 self.file_re = re.compile( 64 space + b"file" + name + b"content" + revision 65 ) 66 self.add_file_re = re.compile( 67 space + b"add_file" + name + b"content" + revision 68 ) 69 self.patch_re = re.compile( 70 space + b"patch" + name + b"from" + revision + b"to" + revision 71 ) 72 self.rename_re = re.compile(space + b"rename" + name + b"to" + name) 73 self.delete_re = re.compile(space + b"delete" + name) 74 self.tag_re = re.compile(space + b"tag" + name + b"revision" + revision) 75 self.cert_re = re.compile( 76 lines + space + b"name" + name + b"value" + value 77 ) 78 79 attr = space + b"file" + lines + space + b"attr" + space 80 self.attr_execute_re = re.compile( 81 attr + b'"mtn:execute"' + space + b'"true"' 82 ) 83 84 # cached data 85 self.manifest_rev = None 86 self.manifest = None 87 self.files = None 88 self.dirs = None 89 90 common.checktool(b'mtn', abort=False) 91 92 def mtnrun(self, *args, **kwargs): 93 if self.automatestdio: 94 return self.mtnrunstdio(*args, **kwargs) 95 else: 96 return self.mtnrunsingle(*args, **kwargs) 97 98 def mtnrunsingle(self, *args, **kwargs): 99 kwargs['d'] = self.path 100 return self.run0(b'automate', *args, **kwargs) 101 102 def mtnrunstdio(self, *args, **kwargs): 103 # Prepare the command in automate stdio format 104 kwargs = pycompat.byteskwargs(kwargs) 105 command = [] 106 for k, v in pycompat.iteritems(kwargs): 107 command.append(b"%d:%s" % (len(k), k)) 108 if v: 109 command.append(b"%d:%s" % (len(v), v)) 110 if command: 111 command.insert(0, b'o') 112 command.append(b'e') 113 114 command.append(b'l') 115 for arg in args: 116 command.append(b"%d:%s" % (len(arg), arg)) 117 command.append(b'e') 118 command = b''.join(command) 119 120 self.ui.debug(b"mtn: sending '%s'\n" % command) 121 self.mtnwritefp.write(command) 122 self.mtnwritefp.flush() 123 124 return self.mtnstdioreadcommandoutput(command) 125 126 def mtnstdioreadpacket(self): 127 read = None 128 commandnbr = b'' 129 while read != b':': 130 read = self.mtnreadfp.read(1) 131 if not read: 132 raise error.Abort(_(b'bad mtn packet - no end of commandnbr')) 133 commandnbr += read 134 commandnbr = commandnbr[:-1] 135 136 stream = self.mtnreadfp.read(1) 137 if stream not in b'mewptl': 138 raise error.Abort( 139 _(b'bad mtn packet - bad stream type %s') % stream 140 ) 141 142 read = self.mtnreadfp.read(1) 143 if read != b':': 144 raise error.Abort(_(b'bad mtn packet - no divider before size')) 145 146 read = None 147 lengthstr = b'' 148 while read != b':': 149 read = self.mtnreadfp.read(1) 150 if not read: 151 raise error.Abort(_(b'bad mtn packet - no end of packet size')) 152 lengthstr += read 153 try: 154 length = pycompat.long(lengthstr[:-1]) 155 except TypeError: 156 raise error.Abort( 157 _(b'bad mtn packet - bad packet size %s') % lengthstr 158 ) 159 160 read = self.mtnreadfp.read(length) 161 if len(read) != length: 162 raise error.Abort( 163 _( 164 b"bad mtn packet - unable to read full packet " 165 b"read %s of %s" 166 ) 167 % (len(read), length) 168 ) 169 170 return (commandnbr, stream, length, read) 171 172 def mtnstdioreadcommandoutput(self, command): 173 retval = [] 174 while True: 175 commandnbr, stream, length, output = self.mtnstdioreadpacket() 176 self.ui.debug( 177 b'mtn: read packet %s:%s:%d\n' % (commandnbr, stream, length) 178 ) 179 180 if stream == b'l': 181 # End of command 182 if output != b'0': 183 raise error.Abort( 184 _(b"mtn command '%s' returned %s") % (command, output) 185 ) 186 break 187 elif stream in b'ew': 188 # Error, warning output 189 self.ui.warn(_(b'%s error:\n') % self.command) 190 self.ui.warn(output) 191 elif stream == b'p': 192 # Progress messages 193 self.ui.debug(b'mtn: ' + output) 194 elif stream == b'm': 195 # Main stream - command output 196 retval.append(output) 197 198 return b''.join(retval) 199 200 def mtnloadmanifest(self, rev): 201 if self.manifest_rev == rev: 202 return 203 self.manifest = self.mtnrun(b"get_manifest_of", rev).split(b"\n\n") 204 self.manifest_rev = rev 205 self.files = {} 206 self.dirs = {} 207 208 for e in self.manifest: 209 m = self.file_re.match(e) 210 if m: 211 attr = b"" 212 name = m.group(1) 213 node = m.group(2) 214 if self.attr_execute_re.match(e): 215 attr += b"x" 216 self.files[name] = (node, attr) 217 m = self.dir_re.match(e) 218 if m: 219 self.dirs[m.group(1)] = True 220 221 def mtnisfile(self, name, rev): 222 # a non-file could be a directory or a deleted or renamed file 223 self.mtnloadmanifest(rev) 224 return name in self.files 225 226 def mtnisdir(self, name, rev): 227 self.mtnloadmanifest(rev) 228 return name in self.dirs 229 230 def mtngetcerts(self, rev): 231 certs = { 232 b"author": b"<missing>", 233 b"date": b"<missing>", 234 b"changelog": b"<missing>", 235 b"branch": b"<missing>", 236 } 237 certlist = self.mtnrun(b"certs", rev) 238 # mtn < 0.45: 239 # key "test@selenic.com" 240 # mtn >= 0.45: 241 # key [ff58a7ffb771907c4ff68995eada1c4da068d328] 242 certlist = re.split(br'\n\n {6}key ["\[]', certlist) 243 for e in certlist: 244 m = self.cert_re.match(e) 245 if m: 246 name, value = m.groups() 247 value = value.replace(br'\"', b'"') 248 value = value.replace(br'\\', b'\\') 249 certs[name] = value 250 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306 251 # and all times are stored in UTC 252 certs[b"date"] = certs[b"date"].split(b'.')[0] + b" UTC" 253 return certs 254 255 # implement the converter_source interface: 256 257 def getheads(self): 258 if not self.revs: 259 return self.mtnrun(b"leaves").splitlines() 260 else: 261 return self.revs 262 263 def getchanges(self, rev, full): 264 if full: 265 raise error.Abort( 266 _(b"convert from monotone does not support --full") 267 ) 268 revision = self.mtnrun(b"get_revision", rev).split(b"\n\n") 269 files = {} 270 ignoremove = {} 271 renameddirs = [] 272 copies = {} 273 for e in revision: 274 m = self.add_file_re.match(e) 275 if m: 276 files[m.group(1)] = rev 277 ignoremove[m.group(1)] = rev 278 m = self.patch_re.match(e) 279 if m: 280 files[m.group(1)] = rev 281 # Delete/rename is handled later when the convert engine 282 # discovers an IOError exception from getfile, 283 # but only if we add the "from" file to the list of changes. 284 m = self.delete_re.match(e) 285 if m: 286 files[m.group(1)] = rev 287 m = self.rename_re.match(e) 288 if m: 289 toname = m.group(2) 290 fromname = m.group(1) 291 if self.mtnisfile(toname, rev): 292 ignoremove[toname] = 1 293 copies[toname] = fromname 294 files[toname] = rev 295 files[fromname] = rev 296 elif self.mtnisdir(toname, rev): 297 renameddirs.append((fromname, toname)) 298 299 # Directory renames can be handled only once we have recorded 300 # all new files 301 for fromdir, todir in renameddirs: 302 renamed = {} 303 for tofile in self.files: 304 if tofile in ignoremove: 305 continue 306 if tofile.startswith(todir + b'/'): 307 renamed[tofile] = fromdir + tofile[len(todir) :] 308 # Avoid chained moves like: 309 # d1(/a) => d3/d1(/a) 310 # d2 => d3 311 ignoremove[tofile] = 1 312 for tofile, fromfile in renamed.items(): 313 self.ui.debug( 314 b"copying file in renamed directory from '%s' to '%s'" 315 % (fromfile, tofile), 316 b'\n', 317 ) 318 files[tofile] = rev 319 copies[tofile] = fromfile 320 for fromfile in renamed.values(): 321 files[fromfile] = rev 322 323 return (files.items(), copies, set()) 324 325 def getfile(self, name, rev): 326 if not self.mtnisfile(name, rev): 327 return None, None 328 try: 329 data = self.mtnrun(b"get_file_of", name, r=rev) 330 except Exception: 331 return None, None 332 self.mtnloadmanifest(rev) 333 node, attr = self.files.get(name, (None, b"")) 334 return data, attr 335 336 def getcommit(self, rev): 337 extra = {} 338 certs = self.mtngetcerts(rev) 339 if certs.get(b'suspend') == certs[b"branch"]: 340 extra[b'close'] = b'1' 341 dateformat = b"%Y-%m-%dT%H:%M:%S" 342 return common.commit( 343 author=certs[b"author"], 344 date=dateutil.datestr(dateutil.strdate(certs[b"date"], dateformat)), 345 desc=certs[b"changelog"], 346 rev=rev, 347 parents=self.mtnrun(b"parents", rev).splitlines(), 348 branch=certs[b"branch"], 349 extra=extra, 350 ) 351 352 def gettags(self): 353 tags = {} 354 for e in self.mtnrun(b"tags").split(b"\n\n"): 355 m = self.tag_re.match(e) 356 if m: 357 tags[m.group(1)] = m.group(2) 358 return tags 359 360 def getchangedfiles(self, rev, i): 361 # This function is only needed to support --filemap 362 # ... and we don't support that 363 raise NotImplementedError 364 365 def before(self): 366 # Check if we have a new enough version to use automate stdio 367 try: 368 versionstr = self.mtnrunsingle(b"interface_version") 369 version = float(versionstr) 370 except Exception: 371 raise error.Abort( 372 _(b"unable to determine mtn automate interface version") 373 ) 374 375 if version >= 12.0: 376 self.automatestdio = True 377 self.ui.debug( 378 b"mtn automate version %f - using automate stdio\n" % version 379 ) 380 381 # launch the long-running automate stdio process 382 self.mtnwritefp, self.mtnreadfp = self._run2( 383 b'automate', b'stdio', b'-d', self.path 384 ) 385 # read the headers 386 read = self.mtnreadfp.readline() 387 if read != b'format-version: 2\n': 388 raise error.Abort( 389 _(b'mtn automate stdio header unexpected: %s') % read 390 ) 391 while read != b'\n': 392 read = self.mtnreadfp.readline() 393 if not read: 394 raise error.Abort( 395 _( 396 b"failed to reach end of mtn automate " 397 b"stdio headers" 398 ) 399 ) 400 else: 401 self.ui.debug( 402 b"mtn automate version %s - not using automate stdio " 403 b"(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version 404 ) 405 406 def after(self): 407 if self.automatestdio: 408 self.mtnwritefp.close() 409 self.mtnwritefp = None 410 self.mtnreadfp.close() 411 self.mtnreadfp = None 412