##// END OF EJS Templates
convert: use raw string for regular expressions...
Gregory Szorc -
r41678:fc09aafd default
parent child Browse files
Show More
@@ -1,373 +1,373 b''
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
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.
7 # GNU General Public License version 2 or any later version.
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial import (
14 from mercurial import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 )
17 )
18 from mercurial.utils import dateutil
18 from mercurial.utils import dateutil
19
19
20 from . import common
20 from . import common
21
21
22 class monotone_source(common.converter_source, common.commandline):
22 class monotone_source(common.converter_source, common.commandline):
23 def __init__(self, ui, repotype, path=None, revs=None):
23 def __init__(self, ui, repotype, path=None, revs=None):
24 common.converter_source.__init__(self, ui, repotype, path, revs)
24 common.converter_source.__init__(self, ui, repotype, path, revs)
25 if revs and len(revs) > 1:
25 if revs and len(revs) > 1:
26 raise error.Abort(_('monotone source does not support specifying '
26 raise error.Abort(_('monotone source does not support specifying '
27 'multiple revs'))
27 'multiple revs'))
28 common.commandline.__init__(self, ui, 'mtn')
28 common.commandline.__init__(self, ui, 'mtn')
29
29
30 self.ui = ui
30 self.ui = ui
31 self.path = path
31 self.path = path
32 self.automatestdio = False
32 self.automatestdio = False
33 self.revs = revs
33 self.revs = revs
34
34
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
36 % path)
36 % path)
37 if not os.path.exists(os.path.join(path, '_MTN')):
37 if not os.path.exists(os.path.join(path, '_MTN')):
38 # Could be a monotone repository (SQLite db file)
38 # Could be a monotone repository (SQLite db file)
39 try:
39 try:
40 f = open(path, 'rb')
40 f = open(path, 'rb')
41 header = f.read(16)
41 header = f.read(16)
42 f.close()
42 f.close()
43 except IOError:
43 except IOError:
44 header = ''
44 header = ''
45 if header != 'SQLite format 3\x00':
45 if header != 'SQLite format 3\x00':
46 raise norepo
46 raise norepo
47
47
48 # regular expressions for parsing monotone output
48 # regular expressions for parsing monotone output
49 space = br'\s*'
49 space = br'\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
51 value = name
51 value = name
52 revision = br'\s+\[(\w+)\]\s*'
52 revision = br'\s+\[(\w+)\]\s*'
53 lines = br'(?:.|\n)+'
53 lines = br'(?:.|\n)+'
54
54
55 self.dir_re = re.compile(space + "dir" + name)
55 self.dir_re = re.compile(space + "dir" + name)
56 self.file_re = re.compile(space + "file" + name +
56 self.file_re = re.compile(space + "file" + name +
57 "content" + revision)
57 "content" + revision)
58 self.add_file_re = re.compile(space + "add_file" + name +
58 self.add_file_re = re.compile(space + "add_file" + name +
59 "content" + revision)
59 "content" + revision)
60 self.patch_re = re.compile(space + "patch" + name +
60 self.patch_re = re.compile(space + "patch" + name +
61 "from" + revision + "to" + revision)
61 "from" + revision + "to" + revision)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
63 self.delete_re = re.compile(space + "delete" + name)
63 self.delete_re = re.compile(space + "delete" + name)
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
65 revision)
65 revision)
66 self.cert_re = re.compile(lines + space + "name" + name +
66 self.cert_re = re.compile(lines + space + "name" + name +
67 "value" + value)
67 "value" + value)
68
68
69 attr = space + "file" + lines + space + "attr" + space
69 attr = space + "file" + lines + space + "attr" + space
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
71 space + '"true"')
71 space + '"true"')
72
72
73 # cached data
73 # cached data
74 self.manifest_rev = None
74 self.manifest_rev = None
75 self.manifest = None
75 self.manifest = None
76 self.files = None
76 self.files = None
77 self.dirs = None
77 self.dirs = None
78
78
79 common.checktool('mtn', abort=False)
79 common.checktool('mtn', abort=False)
80
80
81 def mtnrun(self, *args, **kwargs):
81 def mtnrun(self, *args, **kwargs):
82 if self.automatestdio:
82 if self.automatestdio:
83 return self.mtnrunstdio(*args, **kwargs)
83 return self.mtnrunstdio(*args, **kwargs)
84 else:
84 else:
85 return self.mtnrunsingle(*args, **kwargs)
85 return self.mtnrunsingle(*args, **kwargs)
86
86
87 def mtnrunsingle(self, *args, **kwargs):
87 def mtnrunsingle(self, *args, **kwargs):
88 kwargs[r'd'] = self.path
88 kwargs[r'd'] = self.path
89 return self.run0('automate', *args, **kwargs)
89 return self.run0('automate', *args, **kwargs)
90
90
91 def mtnrunstdio(self, *args, **kwargs):
91 def mtnrunstdio(self, *args, **kwargs):
92 # Prepare the command in automate stdio format
92 # Prepare the command in automate stdio format
93 kwargs = pycompat.byteskwargs(kwargs)
93 kwargs = pycompat.byteskwargs(kwargs)
94 command = []
94 command = []
95 for k, v in kwargs.iteritems():
95 for k, v in kwargs.iteritems():
96 command.append("%d:%s" % (len(k), k))
96 command.append("%d:%s" % (len(k), k))
97 if v:
97 if v:
98 command.append("%d:%s" % (len(v), v))
98 command.append("%d:%s" % (len(v), v))
99 if command:
99 if command:
100 command.insert(0, 'o')
100 command.insert(0, 'o')
101 command.append('e')
101 command.append('e')
102
102
103 command.append('l')
103 command.append('l')
104 for arg in args:
104 for arg in args:
105 command.append("%d:%s" % (len(arg), arg))
105 command.append("%d:%s" % (len(arg), arg))
106 command.append('e')
106 command.append('e')
107 command = ''.join(command)
107 command = ''.join(command)
108
108
109 self.ui.debug("mtn: sending '%s'\n" % command)
109 self.ui.debug("mtn: sending '%s'\n" % command)
110 self.mtnwritefp.write(command)
110 self.mtnwritefp.write(command)
111 self.mtnwritefp.flush()
111 self.mtnwritefp.flush()
112
112
113 return self.mtnstdioreadcommandoutput(command)
113 return self.mtnstdioreadcommandoutput(command)
114
114
115 def mtnstdioreadpacket(self):
115 def mtnstdioreadpacket(self):
116 read = None
116 read = None
117 commandnbr = ''
117 commandnbr = ''
118 while read != ':':
118 while read != ':':
119 read = self.mtnreadfp.read(1)
119 read = self.mtnreadfp.read(1)
120 if not read:
120 if not read:
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
122 commandnbr += read
122 commandnbr += read
123 commandnbr = commandnbr[:-1]
123 commandnbr = commandnbr[:-1]
124
124
125 stream = self.mtnreadfp.read(1)
125 stream = self.mtnreadfp.read(1)
126 if stream not in 'mewptl':
126 if stream not in 'mewptl':
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
128
128
129 read = self.mtnreadfp.read(1)
129 read = self.mtnreadfp.read(1)
130 if read != ':':
130 if read != ':':
131 raise error.Abort(_('bad mtn packet - no divider before size'))
131 raise error.Abort(_('bad mtn packet - no divider before size'))
132
132
133 read = None
133 read = None
134 lengthstr = ''
134 lengthstr = ''
135 while read != ':':
135 while read != ':':
136 read = self.mtnreadfp.read(1)
136 read = self.mtnreadfp.read(1)
137 if not read:
137 if not read:
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
139 lengthstr += read
139 lengthstr += read
140 try:
140 try:
141 length = pycompat.long(lengthstr[:-1])
141 length = pycompat.long(lengthstr[:-1])
142 except TypeError:
142 except TypeError:
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
144 % lengthstr)
144 % lengthstr)
145
145
146 read = self.mtnreadfp.read(length)
146 read = self.mtnreadfp.read(length)
147 if len(read) != length:
147 if len(read) != length:
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
149 "read %s of %s") % (len(read), length))
149 "read %s of %s") % (len(read), length))
150
150
151 return (commandnbr, stream, length, read)
151 return (commandnbr, stream, length, read)
152
152
153 def mtnstdioreadcommandoutput(self, command):
153 def mtnstdioreadcommandoutput(self, command):
154 retval = []
154 retval = []
155 while True:
155 while True:
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
158 (commandnbr, stream, length))
158 (commandnbr, stream, length))
159
159
160 if stream == 'l':
160 if stream == 'l':
161 # End of command
161 # End of command
162 if output != '0':
162 if output != '0':
163 raise error.Abort(_("mtn command '%s' returned %s") %
163 raise error.Abort(_("mtn command '%s' returned %s") %
164 (command, output))
164 (command, output))
165 break
165 break
166 elif stream in 'ew':
166 elif stream in 'ew':
167 # Error, warning output
167 # Error, warning output
168 self.ui.warn(_('%s error:\n') % self.command)
168 self.ui.warn(_('%s error:\n') % self.command)
169 self.ui.warn(output)
169 self.ui.warn(output)
170 elif stream == 'p':
170 elif stream == 'p':
171 # Progress messages
171 # Progress messages
172 self.ui.debug('mtn: ' + output)
172 self.ui.debug('mtn: ' + output)
173 elif stream == 'm':
173 elif stream == 'm':
174 # Main stream - command output
174 # Main stream - command output
175 retval.append(output)
175 retval.append(output)
176
176
177 return ''.join(retval)
177 return ''.join(retval)
178
178
179 def mtnloadmanifest(self, rev):
179 def mtnloadmanifest(self, rev):
180 if self.manifest_rev == rev:
180 if self.manifest_rev == rev:
181 return
181 return
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
183 self.manifest_rev = rev
183 self.manifest_rev = rev
184 self.files = {}
184 self.files = {}
185 self.dirs = {}
185 self.dirs = {}
186
186
187 for e in self.manifest:
187 for e in self.manifest:
188 m = self.file_re.match(e)
188 m = self.file_re.match(e)
189 if m:
189 if m:
190 attr = ""
190 attr = ""
191 name = m.group(1)
191 name = m.group(1)
192 node = m.group(2)
192 node = m.group(2)
193 if self.attr_execute_re.match(e):
193 if self.attr_execute_re.match(e):
194 attr += "x"
194 attr += "x"
195 self.files[name] = (node, attr)
195 self.files[name] = (node, attr)
196 m = self.dir_re.match(e)
196 m = self.dir_re.match(e)
197 if m:
197 if m:
198 self.dirs[m.group(1)] = True
198 self.dirs[m.group(1)] = True
199
199
200 def mtnisfile(self, name, rev):
200 def mtnisfile(self, name, rev):
201 # a non-file could be a directory or a deleted or renamed file
201 # a non-file could be a directory or a deleted or renamed file
202 self.mtnloadmanifest(rev)
202 self.mtnloadmanifest(rev)
203 return name in self.files
203 return name in self.files
204
204
205 def mtnisdir(self, name, rev):
205 def mtnisdir(self, name, rev):
206 self.mtnloadmanifest(rev)
206 self.mtnloadmanifest(rev)
207 return name in self.dirs
207 return name in self.dirs
208
208
209 def mtngetcerts(self, rev):
209 def mtngetcerts(self, rev):
210 certs = {"author":"<missing>", "date":"<missing>",
210 certs = {"author":"<missing>", "date":"<missing>",
211 "changelog":"<missing>", "branch":"<missing>"}
211 "changelog":"<missing>", "branch":"<missing>"}
212 certlist = self.mtnrun("certs", rev)
212 certlist = self.mtnrun("certs", rev)
213 # mtn < 0.45:
213 # mtn < 0.45:
214 # key "test@selenic.com"
214 # key "test@selenic.com"
215 # mtn >= 0.45:
215 # mtn >= 0.45:
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
217 certlist = re.split('\n\n key ["\[]', certlist)
217 certlist = re.split(br'\n\n key ["\[]', certlist)
218 for e in certlist:
218 for e in certlist:
219 m = self.cert_re.match(e)
219 m = self.cert_re.match(e)
220 if m:
220 if m:
221 name, value = m.groups()
221 name, value = m.groups()
222 value = value.replace(r'\"', '"')
222 value = value.replace(r'\"', '"')
223 value = value.replace(r'\\', '\\')
223 value = value.replace(r'\\', '\\')
224 certs[name] = value
224 certs[name] = value
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
226 # and all times are stored in UTC
226 # and all times are stored in UTC
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
228 return certs
228 return certs
229
229
230 # implement the converter_source interface:
230 # implement the converter_source interface:
231
231
232 def getheads(self):
232 def getheads(self):
233 if not self.revs:
233 if not self.revs:
234 return self.mtnrun("leaves").splitlines()
234 return self.mtnrun("leaves").splitlines()
235 else:
235 else:
236 return self.revs
236 return self.revs
237
237
238 def getchanges(self, rev, full):
238 def getchanges(self, rev, full):
239 if full:
239 if full:
240 raise error.Abort(_("convert from monotone does not support "
240 raise error.Abort(_("convert from monotone does not support "
241 "--full"))
241 "--full"))
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
243 files = {}
243 files = {}
244 ignoremove = {}
244 ignoremove = {}
245 renameddirs = []
245 renameddirs = []
246 copies = {}
246 copies = {}
247 for e in revision:
247 for e in revision:
248 m = self.add_file_re.match(e)
248 m = self.add_file_re.match(e)
249 if m:
249 if m:
250 files[m.group(1)] = rev
250 files[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
252 m = self.patch_re.match(e)
252 m = self.patch_re.match(e)
253 if m:
253 if m:
254 files[m.group(1)] = rev
254 files[m.group(1)] = rev
255 # Delete/rename is handled later when the convert engine
255 # Delete/rename is handled later when the convert engine
256 # discovers an IOError exception from getfile,
256 # discovers an IOError exception from getfile,
257 # but only if we add the "from" file to the list of changes.
257 # but only if we add the "from" file to the list of changes.
258 m = self.delete_re.match(e)
258 m = self.delete_re.match(e)
259 if m:
259 if m:
260 files[m.group(1)] = rev
260 files[m.group(1)] = rev
261 m = self.rename_re.match(e)
261 m = self.rename_re.match(e)
262 if m:
262 if m:
263 toname = m.group(2)
263 toname = m.group(2)
264 fromname = m.group(1)
264 fromname = m.group(1)
265 if self.mtnisfile(toname, rev):
265 if self.mtnisfile(toname, rev):
266 ignoremove[toname] = 1
266 ignoremove[toname] = 1
267 copies[toname] = fromname
267 copies[toname] = fromname
268 files[toname] = rev
268 files[toname] = rev
269 files[fromname] = rev
269 files[fromname] = rev
270 elif self.mtnisdir(toname, rev):
270 elif self.mtnisdir(toname, rev):
271 renameddirs.append((fromname, toname))
271 renameddirs.append((fromname, toname))
272
272
273 # Directory renames can be handled only once we have recorded
273 # Directory renames can be handled only once we have recorded
274 # all new files
274 # all new files
275 for fromdir, todir in renameddirs:
275 for fromdir, todir in renameddirs:
276 renamed = {}
276 renamed = {}
277 for tofile in self.files:
277 for tofile in self.files:
278 if tofile in ignoremove:
278 if tofile in ignoremove:
279 continue
279 continue
280 if tofile.startswith(todir + '/'):
280 if tofile.startswith(todir + '/'):
281 renamed[tofile] = fromdir + tofile[len(todir):]
281 renamed[tofile] = fromdir + tofile[len(todir):]
282 # Avoid chained moves like:
282 # Avoid chained moves like:
283 # d1(/a) => d3/d1(/a)
283 # d1(/a) => d3/d1(/a)
284 # d2 => d3
284 # d2 => d3
285 ignoremove[tofile] = 1
285 ignoremove[tofile] = 1
286 for tofile, fromfile in renamed.items():
286 for tofile, fromfile in renamed.items():
287 self.ui.debug (_("copying file in renamed directory "
287 self.ui.debug (_("copying file in renamed directory "
288 "from '%s' to '%s'")
288 "from '%s' to '%s'")
289 % (fromfile, tofile), '\n')
289 % (fromfile, tofile), '\n')
290 files[tofile] = rev
290 files[tofile] = rev
291 copies[tofile] = fromfile
291 copies[tofile] = fromfile
292 for fromfile in renamed.values():
292 for fromfile in renamed.values():
293 files[fromfile] = rev
293 files[fromfile] = rev
294
294
295 return (files.items(), copies, set())
295 return (files.items(), copies, set())
296
296
297 def getfile(self, name, rev):
297 def getfile(self, name, rev):
298 if not self.mtnisfile(name, rev):
298 if not self.mtnisfile(name, rev):
299 return None, None
299 return None, None
300 try:
300 try:
301 data = self.mtnrun("get_file_of", name, r=rev)
301 data = self.mtnrun("get_file_of", name, r=rev)
302 except Exception:
302 except Exception:
303 return None, None
303 return None, None
304 self.mtnloadmanifest(rev)
304 self.mtnloadmanifest(rev)
305 node, attr = self.files.get(name, (None, ""))
305 node, attr = self.files.get(name, (None, ""))
306 return data, attr
306 return data, attr
307
307
308 def getcommit(self, rev):
308 def getcommit(self, rev):
309 extra = {}
309 extra = {}
310 certs = self.mtngetcerts(rev)
310 certs = self.mtngetcerts(rev)
311 if certs.get('suspend') == certs["branch"]:
311 if certs.get('suspend') == certs["branch"]:
312 extra['close'] = 1
312 extra['close'] = 1
313 dateformat = "%Y-%m-%dT%H:%M:%S"
313 dateformat = "%Y-%m-%dT%H:%M:%S"
314 return common.commit(
314 return common.commit(
315 author=certs["author"],
315 author=certs["author"],
316 date=dateutil.datestr(dateutil.strdate(certs["date"], dateformat)),
316 date=dateutil.datestr(dateutil.strdate(certs["date"], dateformat)),
317 desc=certs["changelog"],
317 desc=certs["changelog"],
318 rev=rev,
318 rev=rev,
319 parents=self.mtnrun("parents", rev).splitlines(),
319 parents=self.mtnrun("parents", rev).splitlines(),
320 branch=certs["branch"],
320 branch=certs["branch"],
321 extra=extra)
321 extra=extra)
322
322
323 def gettags(self):
323 def gettags(self):
324 tags = {}
324 tags = {}
325 for e in self.mtnrun("tags").split("\n\n"):
325 for e in self.mtnrun("tags").split("\n\n"):
326 m = self.tag_re.match(e)
326 m = self.tag_re.match(e)
327 if m:
327 if m:
328 tags[m.group(1)] = m.group(2)
328 tags[m.group(1)] = m.group(2)
329 return tags
329 return tags
330
330
331 def getchangedfiles(self, rev, i):
331 def getchangedfiles(self, rev, i):
332 # This function is only needed to support --filemap
332 # This function is only needed to support --filemap
333 # ... and we don't support that
333 # ... and we don't support that
334 raise NotImplementedError
334 raise NotImplementedError
335
335
336 def before(self):
336 def before(self):
337 # Check if we have a new enough version to use automate stdio
337 # Check if we have a new enough version to use automate stdio
338 try:
338 try:
339 versionstr = self.mtnrunsingle("interface_version")
339 versionstr = self.mtnrunsingle("interface_version")
340 version = float(versionstr)
340 version = float(versionstr)
341 except Exception:
341 except Exception:
342 raise error.Abort(_("unable to determine mtn automate interface "
342 raise error.Abort(_("unable to determine mtn automate interface "
343 "version"))
343 "version"))
344
344
345 if version >= 12.0:
345 if version >= 12.0:
346 self.automatestdio = True
346 self.automatestdio = True
347 self.ui.debug("mtn automate version %f - using automate stdio\n" %
347 self.ui.debug("mtn automate version %f - using automate stdio\n" %
348 version)
348 version)
349
349
350 # launch the long-running automate stdio process
350 # launch the long-running automate stdio process
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
352 '-d', self.path)
352 '-d', self.path)
353 # read the headers
353 # read the headers
354 read = self.mtnreadfp.readline()
354 read = self.mtnreadfp.readline()
355 if read != 'format-version: 2\n':
355 if read != 'format-version: 2\n':
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
357 % read)
357 % read)
358 while read != '\n':
358 while read != '\n':
359 read = self.mtnreadfp.readline()
359 read = self.mtnreadfp.readline()
360 if not read:
360 if not read:
361 raise error.Abort(_("failed to reach end of mtn automate "
361 raise error.Abort(_("failed to reach end of mtn automate "
362 "stdio headers"))
362 "stdio headers"))
363 else:
363 else:
364 self.ui.debug("mtn automate version %s - not using automate stdio "
364 self.ui.debug("mtn automate version %s - not using automate stdio "
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
366
366
367 def after(self):
367 def after(self):
368 if self.automatestdio:
368 if self.automatestdio:
369 self.mtnwritefp.close()
369 self.mtnwritefp.close()
370 self.mtnwritefp = None
370 self.mtnwritefp = None
371 self.mtnreadfp.close()
371 self.mtnreadfp.close()
372 self.mtnreadfp = None
372 self.mtnreadfp = None
373
373
@@ -1,378 +1,378 b''
1 # Perforce source for convert extension.
1 # Perforce source for convert extension.
2 #
2 #
3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import marshal
9 import marshal
10 import re
10 import re
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 error,
14 error,
15 util,
15 util,
16 )
16 )
17 from mercurial.utils import (
17 from mercurial.utils import (
18 dateutil,
18 dateutil,
19 procutil,
19 procutil,
20 stringutil,
20 stringutil,
21 )
21 )
22
22
23 from . import common
23 from . import common
24
24
25 def loaditer(f):
25 def loaditer(f):
26 "Yield the dictionary objects generated by p4"
26 "Yield the dictionary objects generated by p4"
27 try:
27 try:
28 while True:
28 while True:
29 d = marshal.load(f)
29 d = marshal.load(f)
30 if not d:
30 if not d:
31 break
31 break
32 yield d
32 yield d
33 except EOFError:
33 except EOFError:
34 pass
34 pass
35
35
36 def decodefilename(filename):
36 def decodefilename(filename):
37 """Perforce escapes special characters @, #, *, or %
37 """Perforce escapes special characters @, #, *, or %
38 with %40, %23, %2A, or %25 respectively
38 with %40, %23, %2A, or %25 respectively
39
39
40 >>> decodefilename(b'portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
40 >>> decodefilename(b'portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
41 'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
41 'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
42 >>> decodefilename(b'//Depot/Directory/%2525/%2523/%23%40.%2A')
42 >>> decodefilename(b'//Depot/Directory/%2525/%2523/%23%40.%2A')
43 '//Depot/Directory/%25/%23/#@.*'
43 '//Depot/Directory/%25/%23/#@.*'
44 """
44 """
45 replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')]
45 replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')]
46 for k, v in replacements:
46 for k, v in replacements:
47 filename = filename.replace(k, v)
47 filename = filename.replace(k, v)
48 return filename
48 return filename
49
49
50 class p4_source(common.converter_source):
50 class p4_source(common.converter_source):
51 def __init__(self, ui, repotype, path, revs=None):
51 def __init__(self, ui, repotype, path, revs=None):
52 # avoid import cycle
52 # avoid import cycle
53 from . import convcmd
53 from . import convcmd
54
54
55 super(p4_source, self).__init__(ui, repotype, path, revs=revs)
55 super(p4_source, self).__init__(ui, repotype, path, revs=revs)
56
56
57 if "/" in path and not path.startswith('//'):
57 if "/" in path and not path.startswith('//'):
58 raise common.NoRepo(_('%s does not look like a P4 repository') %
58 raise common.NoRepo(_('%s does not look like a P4 repository') %
59 path)
59 path)
60
60
61 common.checktool('p4', abort=False)
61 common.checktool('p4', abort=False)
62
62
63 self.revmap = {}
63 self.revmap = {}
64 self.encoding = self.ui.config('convert', 'p4.encoding',
64 self.encoding = self.ui.config('convert', 'p4.encoding',
65 convcmd.orig_encoding)
65 convcmd.orig_encoding)
66 self.re_type = re.compile(
66 self.re_type = re.compile(
67 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
67 br"([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
68 "(\+\w+)?$")
68 br"(\+\w+)?$")
69 self.re_keywords = re.compile(
69 self.re_keywords = re.compile(
70 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
70 br"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
71 r":[^$\n]*\$")
71 br":[^$\n]*\$")
72 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
72 self.re_keywords_old = re.compile(br"\$(Id|Header):[^$\n]*\$")
73
73
74 if revs and len(revs) > 1:
74 if revs and len(revs) > 1:
75 raise error.Abort(_("p4 source does not support specifying "
75 raise error.Abort(_("p4 source does not support specifying "
76 "multiple revisions"))
76 "multiple revisions"))
77
77
78 def setrevmap(self, revmap):
78 def setrevmap(self, revmap):
79 """Sets the parsed revmap dictionary.
79 """Sets the parsed revmap dictionary.
80
80
81 Revmap stores mappings from a source revision to a target revision.
81 Revmap stores mappings from a source revision to a target revision.
82 It is set in convertcmd.convert and provided by the user as a file
82 It is set in convertcmd.convert and provided by the user as a file
83 on the commandline.
83 on the commandline.
84
84
85 Revisions in the map are considered beeing present in the
85 Revisions in the map are considered beeing present in the
86 repository and ignored during _parse(). This allows for incremental
86 repository and ignored during _parse(). This allows for incremental
87 imports if a revmap is provided.
87 imports if a revmap is provided.
88 """
88 """
89 self.revmap = revmap
89 self.revmap = revmap
90
90
91 def _parse_view(self, path):
91 def _parse_view(self, path):
92 "Read changes affecting the path"
92 "Read changes affecting the path"
93 cmd = 'p4 -G changes -s submitted %s' % procutil.shellquote(path)
93 cmd = 'p4 -G changes -s submitted %s' % procutil.shellquote(path)
94 stdout = procutil.popen(cmd, mode='rb')
94 stdout = procutil.popen(cmd, mode='rb')
95 p4changes = {}
95 p4changes = {}
96 for d in loaditer(stdout):
96 for d in loaditer(stdout):
97 c = d.get("change", None)
97 c = d.get("change", None)
98 if c:
98 if c:
99 p4changes[c] = True
99 p4changes[c] = True
100 return p4changes
100 return p4changes
101
101
102 def _parse(self, ui, path):
102 def _parse(self, ui, path):
103 "Prepare list of P4 filenames and revisions to import"
103 "Prepare list of P4 filenames and revisions to import"
104 p4changes = {}
104 p4changes = {}
105 changeset = {}
105 changeset = {}
106 files_map = {}
106 files_map = {}
107 copies_map = {}
107 copies_map = {}
108 localname = {}
108 localname = {}
109 depotname = {}
109 depotname = {}
110 heads = []
110 heads = []
111
111
112 ui.status(_('reading p4 views\n'))
112 ui.status(_('reading p4 views\n'))
113
113
114 # read client spec or view
114 # read client spec or view
115 if "/" in path:
115 if "/" in path:
116 p4changes.update(self._parse_view(path))
116 p4changes.update(self._parse_view(path))
117 if path.startswith("//") and path.endswith("/..."):
117 if path.startswith("//") and path.endswith("/..."):
118 views = {path[:-3]:""}
118 views = {path[:-3]:""}
119 else:
119 else:
120 views = {"//": ""}
120 views = {"//": ""}
121 else:
121 else:
122 cmd = 'p4 -G client -o %s' % procutil.shellquote(path)
122 cmd = 'p4 -G client -o %s' % procutil.shellquote(path)
123 clientspec = marshal.load(procutil.popen(cmd, mode='rb'))
123 clientspec = marshal.load(procutil.popen(cmd, mode='rb'))
124
124
125 views = {}
125 views = {}
126 for client in clientspec:
126 for client in clientspec:
127 if client.startswith("View"):
127 if client.startswith("View"):
128 sview, cview = clientspec[client].split()
128 sview, cview = clientspec[client].split()
129 p4changes.update(self._parse_view(sview))
129 p4changes.update(self._parse_view(sview))
130 if sview.endswith("...") and cview.endswith("..."):
130 if sview.endswith("...") and cview.endswith("..."):
131 sview = sview[:-3]
131 sview = sview[:-3]
132 cview = cview[:-3]
132 cview = cview[:-3]
133 cview = cview[2:]
133 cview = cview[2:]
134 cview = cview[cview.find("/") + 1:]
134 cview = cview[cview.find("/") + 1:]
135 views[sview] = cview
135 views[sview] = cview
136
136
137 # list of changes that affect our source files
137 # list of changes that affect our source files
138 p4changes = p4changes.keys()
138 p4changes = p4changes.keys()
139 p4changes.sort(key=int)
139 p4changes.sort(key=int)
140
140
141 # list with depot pathnames, longest first
141 # list with depot pathnames, longest first
142 vieworder = views.keys()
142 vieworder = views.keys()
143 vieworder.sort(key=len, reverse=True)
143 vieworder.sort(key=len, reverse=True)
144
144
145 # handle revision limiting
145 # handle revision limiting
146 startrev = self.ui.config('convert', 'p4.startrev')
146 startrev = self.ui.config('convert', 'p4.startrev')
147
147
148 # now read the full changelists to get the list of file revisions
148 # now read the full changelists to get the list of file revisions
149 ui.status(_('collecting p4 changelists\n'))
149 ui.status(_('collecting p4 changelists\n'))
150 lastid = None
150 lastid = None
151 for change in p4changes:
151 for change in p4changes:
152 if startrev and int(change) < int(startrev):
152 if startrev and int(change) < int(startrev):
153 continue
153 continue
154 if self.revs and int(change) > int(self.revs[0]):
154 if self.revs and int(change) > int(self.revs[0]):
155 continue
155 continue
156 if change in self.revmap:
156 if change in self.revmap:
157 # Ignore already present revisions, but set the parent pointer.
157 # Ignore already present revisions, but set the parent pointer.
158 lastid = change
158 lastid = change
159 continue
159 continue
160
160
161 if lastid:
161 if lastid:
162 parents = [lastid]
162 parents = [lastid]
163 else:
163 else:
164 parents = []
164 parents = []
165
165
166 d = self._fetch_revision(change)
166 d = self._fetch_revision(change)
167 c = self._construct_commit(d, parents)
167 c = self._construct_commit(d, parents)
168
168
169 descarr = c.desc.splitlines(True)
169 descarr = c.desc.splitlines(True)
170 if len(descarr) > 0:
170 if len(descarr) > 0:
171 shortdesc = descarr[0].rstrip('\r\n')
171 shortdesc = descarr[0].rstrip('\r\n')
172 else:
172 else:
173 shortdesc = '**empty changelist description**'
173 shortdesc = '**empty changelist description**'
174
174
175 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
175 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
176 ui.status(stringutil.ellipsis(t, 80) + '\n')
176 ui.status(stringutil.ellipsis(t, 80) + '\n')
177
177
178 files = []
178 files = []
179 copies = {}
179 copies = {}
180 copiedfiles = []
180 copiedfiles = []
181 i = 0
181 i = 0
182 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
182 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
183 oldname = d["depotFile%d" % i]
183 oldname = d["depotFile%d" % i]
184 filename = None
184 filename = None
185 for v in vieworder:
185 for v in vieworder:
186 if oldname.lower().startswith(v.lower()):
186 if oldname.lower().startswith(v.lower()):
187 filename = decodefilename(views[v] + oldname[len(v):])
187 filename = decodefilename(views[v] + oldname[len(v):])
188 break
188 break
189 if filename:
189 if filename:
190 files.append((filename, d["rev%d" % i]))
190 files.append((filename, d["rev%d" % i]))
191 depotname[filename] = oldname
191 depotname[filename] = oldname
192 if (d.get("action%d" % i) == "move/add"):
192 if (d.get("action%d" % i) == "move/add"):
193 copiedfiles.append(filename)
193 copiedfiles.append(filename)
194 localname[oldname] = filename
194 localname[oldname] = filename
195 i += 1
195 i += 1
196
196
197 # Collect information about copied files
197 # Collect information about copied files
198 for filename in copiedfiles:
198 for filename in copiedfiles:
199 oldname = depotname[filename]
199 oldname = depotname[filename]
200
200
201 flcmd = 'p4 -G filelog %s' \
201 flcmd = 'p4 -G filelog %s' \
202 % procutil.shellquote(oldname)
202 % procutil.shellquote(oldname)
203 flstdout = procutil.popen(flcmd, mode='rb')
203 flstdout = procutil.popen(flcmd, mode='rb')
204
204
205 copiedfilename = None
205 copiedfilename = None
206 for d in loaditer(flstdout):
206 for d in loaditer(flstdout):
207 copiedoldname = None
207 copiedoldname = None
208
208
209 i = 0
209 i = 0
210 while ("change%d" % i) in d:
210 while ("change%d" % i) in d:
211 if (d["change%d" % i] == change and
211 if (d["change%d" % i] == change and
212 d["action%d" % i] == "move/add"):
212 d["action%d" % i] == "move/add"):
213 j = 0
213 j = 0
214 while ("file%d,%d" % (i, j)) in d:
214 while ("file%d,%d" % (i, j)) in d:
215 if d["how%d,%d" % (i, j)] == "moved from":
215 if d["how%d,%d" % (i, j)] == "moved from":
216 copiedoldname = d["file%d,%d" % (i, j)]
216 copiedoldname = d["file%d,%d" % (i, j)]
217 break
217 break
218 j += 1
218 j += 1
219 i += 1
219 i += 1
220
220
221 if copiedoldname and copiedoldname in localname:
221 if copiedoldname and copiedoldname in localname:
222 copiedfilename = localname[copiedoldname]
222 copiedfilename = localname[copiedoldname]
223 break
223 break
224
224
225 if copiedfilename:
225 if copiedfilename:
226 copies[filename] = copiedfilename
226 copies[filename] = copiedfilename
227 else:
227 else:
228 ui.warn(_("cannot find source for copied file: %s@%s\n")
228 ui.warn(_("cannot find source for copied file: %s@%s\n")
229 % (filename, change))
229 % (filename, change))
230
230
231 changeset[change] = c
231 changeset[change] = c
232 files_map[change] = files
232 files_map[change] = files
233 copies_map[change] = copies
233 copies_map[change] = copies
234 lastid = change
234 lastid = change
235
235
236 if lastid and len(changeset) > 0:
236 if lastid and len(changeset) > 0:
237 heads = [lastid]
237 heads = [lastid]
238
238
239 return {
239 return {
240 'changeset': changeset,
240 'changeset': changeset,
241 'files': files_map,
241 'files': files_map,
242 'copies': copies_map,
242 'copies': copies_map,
243 'heads': heads,
243 'heads': heads,
244 'depotname': depotname,
244 'depotname': depotname,
245 }
245 }
246
246
247 @util.propertycache
247 @util.propertycache
248 def _parse_once(self):
248 def _parse_once(self):
249 return self._parse(self.ui, self.path)
249 return self._parse(self.ui, self.path)
250
250
251 @util.propertycache
251 @util.propertycache
252 def copies(self):
252 def copies(self):
253 return self._parse_once['copies']
253 return self._parse_once['copies']
254
254
255 @util.propertycache
255 @util.propertycache
256 def files(self):
256 def files(self):
257 return self._parse_once['files']
257 return self._parse_once['files']
258
258
259 @util.propertycache
259 @util.propertycache
260 def changeset(self):
260 def changeset(self):
261 return self._parse_once['changeset']
261 return self._parse_once['changeset']
262
262
263 @util.propertycache
263 @util.propertycache
264 def heads(self):
264 def heads(self):
265 return self._parse_once['heads']
265 return self._parse_once['heads']
266
266
267 @util.propertycache
267 @util.propertycache
268 def depotname(self):
268 def depotname(self):
269 return self._parse_once['depotname']
269 return self._parse_once['depotname']
270
270
271 def getheads(self):
271 def getheads(self):
272 return self.heads
272 return self.heads
273
273
274 def getfile(self, name, rev):
274 def getfile(self, name, rev):
275 cmd = 'p4 -G print %s' \
275 cmd = 'p4 -G print %s' \
276 % procutil.shellquote("%s#%s" % (self.depotname[name], rev))
276 % procutil.shellquote("%s#%s" % (self.depotname[name], rev))
277
277
278 lasterror = None
278 lasterror = None
279 while True:
279 while True:
280 stdout = procutil.popen(cmd, mode='rb')
280 stdout = procutil.popen(cmd, mode='rb')
281
281
282 mode = None
282 mode = None
283 contents = []
283 contents = []
284 keywords = None
284 keywords = None
285
285
286 for d in loaditer(stdout):
286 for d in loaditer(stdout):
287 code = d["code"]
287 code = d["code"]
288 data = d.get("data")
288 data = d.get("data")
289
289
290 if code == "error":
290 if code == "error":
291 # if this is the first time error happened
291 # if this is the first time error happened
292 # re-attempt getting the file
292 # re-attempt getting the file
293 if not lasterror:
293 if not lasterror:
294 lasterror = IOError(d["generic"], data)
294 lasterror = IOError(d["generic"], data)
295 # this will exit inner-most for-loop
295 # this will exit inner-most for-loop
296 break
296 break
297 else:
297 else:
298 raise lasterror
298 raise lasterror
299
299
300 elif code == "stat":
300 elif code == "stat":
301 action = d.get("action")
301 action = d.get("action")
302 if action in ["purge", "delete", "move/delete"]:
302 if action in ["purge", "delete", "move/delete"]:
303 return None, None
303 return None, None
304 p4type = self.re_type.match(d["type"])
304 p4type = self.re_type.match(d["type"])
305 if p4type:
305 if p4type:
306 mode = ""
306 mode = ""
307 flags = ((p4type.group(1) or "")
307 flags = ((p4type.group(1) or "")
308 + (p4type.group(3) or ""))
308 + (p4type.group(3) or ""))
309 if "x" in flags:
309 if "x" in flags:
310 mode = "x"
310 mode = "x"
311 if p4type.group(2) == "symlink":
311 if p4type.group(2) == "symlink":
312 mode = "l"
312 mode = "l"
313 if "ko" in flags:
313 if "ko" in flags:
314 keywords = self.re_keywords_old
314 keywords = self.re_keywords_old
315 elif "k" in flags:
315 elif "k" in flags:
316 keywords = self.re_keywords
316 keywords = self.re_keywords
317
317
318 elif code == "text" or code == "binary":
318 elif code == "text" or code == "binary":
319 contents.append(data)
319 contents.append(data)
320
320
321 lasterror = None
321 lasterror = None
322
322
323 if not lasterror:
323 if not lasterror:
324 break
324 break
325
325
326 if mode is None:
326 if mode is None:
327 return None, None
327 return None, None
328
328
329 contents = ''.join(contents)
329 contents = ''.join(contents)
330
330
331 if keywords:
331 if keywords:
332 contents = keywords.sub("$\\1$", contents)
332 contents = keywords.sub("$\\1$", contents)
333 if mode == "l" and contents.endswith("\n"):
333 if mode == "l" and contents.endswith("\n"):
334 contents = contents[:-1]
334 contents = contents[:-1]
335
335
336 return contents, mode
336 return contents, mode
337
337
338 def getchanges(self, rev, full):
338 def getchanges(self, rev, full):
339 if full:
339 if full:
340 raise error.Abort(_("convert from p4 does not support --full"))
340 raise error.Abort(_("convert from p4 does not support --full"))
341 return self.files[rev], self.copies[rev], set()
341 return self.files[rev], self.copies[rev], set()
342
342
343 def _construct_commit(self, obj, parents=None):
343 def _construct_commit(self, obj, parents=None):
344 """
344 """
345 Constructs a common.commit object from an unmarshalled
345 Constructs a common.commit object from an unmarshalled
346 `p4 describe` output
346 `p4 describe` output
347 """
347 """
348 desc = self.recode(obj.get("desc", ""))
348 desc = self.recode(obj.get("desc", ""))
349 date = (int(obj["time"]), 0) # timezone not set
349 date = (int(obj["time"]), 0) # timezone not set
350 if parents is None:
350 if parents is None:
351 parents = []
351 parents = []
352
352
353 return common.commit(author=self.recode(obj["user"]),
353 return common.commit(author=self.recode(obj["user"]),
354 date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
354 date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
355 parents=parents, desc=desc, branch=None, rev=obj['change'],
355 parents=parents, desc=desc, branch=None, rev=obj['change'],
356 extra={"p4": obj['change'], "convert_revision": obj['change']})
356 extra={"p4": obj['change'], "convert_revision": obj['change']})
357
357
358 def _fetch_revision(self, rev):
358 def _fetch_revision(self, rev):
359 """Return an output of `p4 describe` including author, commit date as
359 """Return an output of `p4 describe` including author, commit date as
360 a dictionary."""
360 a dictionary."""
361 cmd = "p4 -G describe -s %s" % rev
361 cmd = "p4 -G describe -s %s" % rev
362 stdout = procutil.popen(cmd, mode='rb')
362 stdout = procutil.popen(cmd, mode='rb')
363 return marshal.load(stdout)
363 return marshal.load(stdout)
364
364
365 def getcommit(self, rev):
365 def getcommit(self, rev):
366 if rev in self.changeset:
366 if rev in self.changeset:
367 return self.changeset[rev]
367 return self.changeset[rev]
368 elif rev in self.revmap:
368 elif rev in self.revmap:
369 d = self._fetch_revision(rev)
369 d = self._fetch_revision(rev)
370 return self._construct_commit(d, parents=None)
370 return self._construct_commit(d, parents=None)
371 raise error.Abort(
371 raise error.Abort(
372 _("cannot find %s in the revmap or parsed changesets") % rev)
372 _("cannot find %s in the revmap or parsed changesets") % rev)
373
373
374 def gettags(self):
374 def gettags(self):
375 return {}
375 return {}
376
376
377 def getchangedfiles(self, rev, i):
377 def getchangedfiles(self, rev, i):
378 return sorted([x[0] for x in self.files[rev]])
378 return sorted([x[0] for x in self.files[rev]])
General Comments 0
You need to be logged in to leave comments. Login now