##// END OF EJS Templates
convert: return calculated values from parse() instead of manpulating state
David Soria Parra -
r30631:c2be48e5 default
parent child Browse files
Show More
@@ -1,336 +1,358 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
17
18 from . import common
18 from . import common
19
19
20 def loaditer(f):
20 def loaditer(f):
21 "Yield the dictionary objects generated by p4"
21 "Yield the dictionary objects generated by p4"
22 try:
22 try:
23 while True:
23 while True:
24 d = marshal.load(f)
24 d = marshal.load(f)
25 if not d:
25 if not d:
26 break
26 break
27 yield d
27 yield d
28 except EOFError:
28 except EOFError:
29 pass
29 pass
30
30
31 def decodefilename(filename):
31 def decodefilename(filename):
32 """Perforce escapes special characters @, #, *, or %
32 """Perforce escapes special characters @, #, *, or %
33 with %40, %23, %2A, or %25 respectively
33 with %40, %23, %2A, or %25 respectively
34
34
35 >>> decodefilename('portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
35 >>> decodefilename('portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
36 'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
36 'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
37 >>> decodefilename('//Depot/Directory/%2525/%2523/%23%40.%2A')
37 >>> decodefilename('//Depot/Directory/%2525/%2523/%23%40.%2A')
38 '//Depot/Directory/%25/%23/#@.*'
38 '//Depot/Directory/%25/%23/#@.*'
39 """
39 """
40 replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')]
40 replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')]
41 for k, v in replacements:
41 for k, v in replacements:
42 filename = filename.replace(k, v)
42 filename = filename.replace(k, v)
43 return filename
43 return filename
44
44
45 class p4_source(common.converter_source):
45 class p4_source(common.converter_source):
46 def __init__(self, ui, path, revs=None):
46 def __init__(self, ui, path, revs=None):
47 # avoid import cycle
47 # avoid import cycle
48 from . import convcmd
48 from . import convcmd
49
49
50 super(p4_source, self).__init__(ui, path, revs=revs)
50 super(p4_source, self).__init__(ui, path, revs=revs)
51
51
52 if "/" in path and not path.startswith('//'):
52 if "/" in path and not path.startswith('//'):
53 raise common.NoRepo(_('%s does not look like a P4 repository') %
53 raise common.NoRepo(_('%s does not look like a P4 repository') %
54 path)
54 path)
55
55
56 common.checktool('p4', abort=False)
56 common.checktool('p4', abort=False)
57
57
58 self.revmap = {}
58 self.revmap = {}
59 self.p4changes = {}
60 self.heads = []
59 self.heads = []
61 self.changeset = {}
60 self.changeset = {}
62 self.files = {}
61 self.files = {}
63 self.copies = {}
62 self.copies = {}
64 self.encoding = self.ui.config('convert', 'p4.encoding',
63 self.encoding = self.ui.config('convert', 'p4.encoding',
65 default=convcmd.orig_encoding)
64 default=convcmd.orig_encoding)
66 self.depotname = {} # mapping from local name to depot name
65 self.depotname = {} # mapping from local name to depot name
67 self.re_type = re.compile(
66 self.re_type = re.compile(
68 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
67 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
69 "(\+\w+)?$")
68 "(\+\w+)?$")
70 self.re_keywords = re.compile(
69 self.re_keywords = re.compile(
71 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
70 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
72 r":[^$\n]*\$")
71 r":[^$\n]*\$")
73 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
72 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
74
73
75 if revs and len(revs) > 1:
74 if revs and len(revs) > 1:
76 raise error.Abort(_("p4 source does not support specifying "
75 raise error.Abort(_("p4 source does not support specifying "
77 "multiple revisions"))
76 "multiple revisions"))
78 self._parse(ui, path)
77 self._parse_once(ui, path)
79
78
80 def setrevmap(self, revmap):
79 def setrevmap(self, revmap):
81 """Sets the parsed revmap dictionary.
80 """Sets the parsed revmap dictionary.
82
81
83 Revmap stores mappings from a source revision to a target revision.
82 Revmap stores mappings from a source revision to a target revision.
84 It is set in convertcmd.convert and provided by the user as a file
83 It is set in convertcmd.convert and provided by the user as a file
85 on the commandline.
84 on the commandline.
86
85
87 Revisions in the map are considered beeing present in the
86 Revisions in the map are considered beeing present in the
88 repository and ignored during _parse(). This allows for incremental
87 repository and ignored during _parse(). This allows for incremental
89 imports if a revmap is provided.
88 imports if a revmap is provided.
90 """
89 """
91 self.revmap = revmap
90 self.revmap = revmap
92
91
93 def _parse_view(self, path):
92 def _parse_view(self, path):
94 "Read changes affecting the path"
93 "Read changes affecting the path"
95 cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
94 cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
96 stdout = util.popen(cmd, mode='rb')
95 stdout = util.popen(cmd, mode='rb')
97 p4changes = {}
96 p4changes = {}
98 for d in loaditer(stdout):
97 for d in loaditer(stdout):
99 c = d.get("change", None)
98 c = d.get("change", None)
100 if c:
99 if c:
101 p4changes[c] = True
100 p4changes[c] = True
102 return p4changes
101 return p4changes
103
102
104 def _parse(self, ui, path):
103 def _parse(self, ui, path):
105 "Prepare list of P4 filenames and revisions to import"
104 "Prepare list of P4 filenames and revisions to import"
105 p4changes = {}
106 changeset = {}
107 files_map = {}
108 copies_map = {}
109 localname = {}
110 depotname = {}
111 heads = []
112
106 ui.status(_('reading p4 views\n'))
113 ui.status(_('reading p4 views\n'))
107
114
108 # read client spec or view
115 # read client spec or view
109 if "/" in path:
116 if "/" in path:
110 self.p4changes.update(self._parse_view(path))
117 p4changes.update(self._parse_view(path))
111 if path.startswith("//") and path.endswith("/..."):
118 if path.startswith("//") and path.endswith("/..."):
112 views = {path[:-3]:""}
119 views = {path[:-3]:""}
113 else:
120 else:
114 views = {"//": ""}
121 views = {"//": ""}
115 else:
122 else:
116 cmd = 'p4 -G client -o %s' % util.shellquote(path)
123 cmd = 'p4 -G client -o %s' % util.shellquote(path)
117 clientspec = marshal.load(util.popen(cmd, mode='rb'))
124 clientspec = marshal.load(util.popen(cmd, mode='rb'))
118
125
119 views = {}
126 views = {}
120 for client in clientspec:
127 for client in clientspec:
121 if client.startswith("View"):
128 if client.startswith("View"):
122 sview, cview = clientspec[client].split()
129 sview, cview = clientspec[client].split()
123 self.p4changes.update(self._parse_view(sview))
130 p4changes.update(self._parse_view(sview))
124 if sview.endswith("...") and cview.endswith("..."):
131 if sview.endswith("...") and cview.endswith("..."):
125 sview = sview[:-3]
132 sview = sview[:-3]
126 cview = cview[:-3]
133 cview = cview[:-3]
127 cview = cview[2:]
134 cview = cview[2:]
128 cview = cview[cview.find("/") + 1:]
135 cview = cview[cview.find("/") + 1:]
129 views[sview] = cview
136 views[sview] = cview
130
137
131 # list of changes that affect our source files
138 # list of changes that affect our source files
132 self.p4changes = self.p4changes.keys()
139 p4changes = p4changes.keys()
133 self.p4changes.sort(key=int)
140 p4changes.sort(key=int)
134
141
135 # list with depot pathnames, longest first
142 # list with depot pathnames, longest first
136 vieworder = views.keys()
143 vieworder = views.keys()
137 vieworder.sort(key=len, reverse=True)
144 vieworder.sort(key=len, reverse=True)
138
145
139 # handle revision limiting
146 # handle revision limiting
140 startrev = self.ui.config('convert', 'p4.startrev', default=0)
147 startrev = self.ui.config('convert', 'p4.startrev', default=0)
141
148
142 # now read the full changelists to get the list of file revisions
149 # now read the full changelists to get the list of file revisions
143 ui.status(_('collecting p4 changelists\n'))
150 ui.status(_('collecting p4 changelists\n'))
144 lastid = None
151 lastid = None
145 for change in self.p4changes:
152 for change in p4changes:
146 if startrev and int(change) < int(startrev):
153 if startrev and int(change) < int(startrev):
147 continue
154 continue
148 if self.revs and int(change) > int(self.revs[0]):
155 if self.revs and int(change) > int(self.revs[0]):
149 continue
156 continue
150 if change in self.revmap:
157 if change in self.revmap:
151 # Ignore already present revisions, but set the parent pointer.
158 # Ignore already present revisions, but set the parent pointer.
152 lastid = change
159 lastid = change
153 continue
160 continue
154
161
155 if lastid:
162 if lastid:
156 parents = [lastid]
163 parents = [lastid]
157 else:
164 else:
158 parents = []
165 parents = []
159
166
160 d = self._fetch_revision(change)
167 d = self._fetch_revision(change)
161 c = self._construct_commit(d, parents)
168 c = self._construct_commit(d, parents)
162
169
163 shortdesc = c.desc.splitlines(True)[0].rstrip('\r\n')
170 shortdesc = c.desc.splitlines(True)[0].rstrip('\r\n')
164 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
171 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
165 ui.status(util.ellipsis(t, 80) + '\n')
172 ui.status(util.ellipsis(t, 80) + '\n')
166
173
167 files = []
174 files = []
168 copies = {}
175 copies = {}
169 copiedfiles = []
176 copiedfiles = []
170 localname = {}
171 i = 0
177 i = 0
172 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
178 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
173 oldname = d["depotFile%d" % i]
179 oldname = d["depotFile%d" % i]
174 filename = None
180 filename = None
175 for v in vieworder:
181 for v in vieworder:
176 if oldname.lower().startswith(v.lower()):
182 if oldname.lower().startswith(v.lower()):
177 filename = decodefilename(views[v] + oldname[len(v):])
183 filename = decodefilename(views[v] + oldname[len(v):])
178 break
184 break
179 if filename:
185 if filename:
180 files.append((filename, d["rev%d" % i]))
186 files.append((filename, d["rev%d" % i]))
181 self.depotname[filename] = oldname
187 depotname[filename] = oldname
182 if (d.get("action%d" % i) == "move/add"):
188 if (d.get("action%d" % i) == "move/add"):
183 copiedfiles.append(filename)
189 copiedfiles.append(filename)
184 localname[oldname] = filename
190 localname[oldname] = filename
185 i += 1
191 i += 1
186
192
187 # Collect information about copied files
193 # Collect information about copied files
188 for filename in copiedfiles:
194 for filename in copiedfiles:
189 oldname = self.depotname[filename]
195 oldname = depotname[filename]
190
196
191 flcmd = 'p4 -G filelog %s' \
197 flcmd = 'p4 -G filelog %s' \
192 % util.shellquote(oldname)
198 % util.shellquote(oldname)
193 flstdout = util.popen(flcmd, mode='rb')
199 flstdout = util.popen(flcmd, mode='rb')
194
200
195 copiedfilename = None
201 copiedfilename = None
196 for d in loaditer(flstdout):
202 for d in loaditer(flstdout):
197 copiedoldname = None
203 copiedoldname = None
198
204
199 i = 0
205 i = 0
200 while ("change%d" % i) in d:
206 while ("change%d" % i) in d:
201 if (d["change%d" % i] == change and
207 if (d["change%d" % i] == change and
202 d["action%d" % i] == "move/add"):
208 d["action%d" % i] == "move/add"):
203 j = 0
209 j = 0
204 while ("file%d,%d" % (i, j)) in d:
210 while ("file%d,%d" % (i, j)) in d:
205 if d["how%d,%d" % (i, j)] == "moved from":
211 if d["how%d,%d" % (i, j)] == "moved from":
206 copiedoldname = d["file%d,%d" % (i, j)]
212 copiedoldname = d["file%d,%d" % (i, j)]
207 break
213 break
208 j += 1
214 j += 1
209 i += 1
215 i += 1
210
216
211 if copiedoldname and copiedoldname in localname:
217 if copiedoldname and copiedoldname in localname:
212 copiedfilename = localname[copiedoldname]
218 copiedfilename = localname[copiedoldname]
213 break
219 break
214
220
215 if copiedfilename:
221 if copiedfilename:
216 copies[filename] = copiedfilename
222 copies[filename] = copiedfilename
217 else:
223 else:
218 ui.warn(_("cannot find source for copied file: %s@%s\n")
224 ui.warn(_("cannot find source for copied file: %s@%s\n")
219 % (filename, change))
225 % (filename, change))
220
226
221 self.changeset[change] = c
227 changeset[change] = c
222 self.files[change] = files
228 files_map[change] = files
223 self.copies[change] = copies
229 copies_map[change] = copies
224 lastid = change
230 lastid = change
225
231
226 if lastid and len(self.changeset) > 0:
232 if lastid and len(changeset) > 0:
227 self.heads = [lastid]
233 heads = [lastid]
234
235 return {
236 'changeset': changeset,
237 'files': files_map,
238 'copies': copies_map,
239 'heads': heads,
240 'depotname': depotname,
241 }
242
243 def _parse_once(self, ui, path):
244 d = self._parse(ui, path)
245 self.changeset = d['changeset']
246 self.heads = d['heads']
247 self.files = d['files']
248 self.copies = d['copies']
249 self.depotname = d['depotname']
228
250
229 def getheads(self):
251 def getheads(self):
230 return self.heads
252 return self.heads
231
253
232 def getfile(self, name, rev):
254 def getfile(self, name, rev):
233 cmd = 'p4 -G print %s' \
255 cmd = 'p4 -G print %s' \
234 % util.shellquote("%s#%s" % (self.depotname[name], rev))
256 % util.shellquote("%s#%s" % (self.depotname[name], rev))
235
257
236 lasterror = None
258 lasterror = None
237 while True:
259 while True:
238 stdout = util.popen(cmd, mode='rb')
260 stdout = util.popen(cmd, mode='rb')
239
261
240 mode = None
262 mode = None
241 contents = []
263 contents = []
242 keywords = None
264 keywords = None
243
265
244 for d in loaditer(stdout):
266 for d in loaditer(stdout):
245 code = d["code"]
267 code = d["code"]
246 data = d.get("data")
268 data = d.get("data")
247
269
248 if code == "error":
270 if code == "error":
249 # if this is the first time error happened
271 # if this is the first time error happened
250 # re-attempt getting the file
272 # re-attempt getting the file
251 if not lasterror:
273 if not lasterror:
252 lasterror = IOError(d["generic"], data)
274 lasterror = IOError(d["generic"], data)
253 # this will exit inner-most for-loop
275 # this will exit inner-most for-loop
254 break
276 break
255 else:
277 else:
256 raise lasterror
278 raise lasterror
257
279
258 elif code == "stat":
280 elif code == "stat":
259 action = d.get("action")
281 action = d.get("action")
260 if action in ["purge", "delete", "move/delete"]:
282 if action in ["purge", "delete", "move/delete"]:
261 return None, None
283 return None, None
262 p4type = self.re_type.match(d["type"])
284 p4type = self.re_type.match(d["type"])
263 if p4type:
285 if p4type:
264 mode = ""
286 mode = ""
265 flags = ((p4type.group(1) or "")
287 flags = ((p4type.group(1) or "")
266 + (p4type.group(3) or ""))
288 + (p4type.group(3) or ""))
267 if "x" in flags:
289 if "x" in flags:
268 mode = "x"
290 mode = "x"
269 if p4type.group(2) == "symlink":
291 if p4type.group(2) == "symlink":
270 mode = "l"
292 mode = "l"
271 if "ko" in flags:
293 if "ko" in flags:
272 keywords = self.re_keywords_old
294 keywords = self.re_keywords_old
273 elif "k" in flags:
295 elif "k" in flags:
274 keywords = self.re_keywords
296 keywords = self.re_keywords
275
297
276 elif code == "text" or code == "binary":
298 elif code == "text" or code == "binary":
277 contents.append(data)
299 contents.append(data)
278
300
279 lasterror = None
301 lasterror = None
280
302
281 if not lasterror:
303 if not lasterror:
282 break
304 break
283
305
284 if mode is None:
306 if mode is None:
285 return None, None
307 return None, None
286
308
287 contents = ''.join(contents)
309 contents = ''.join(contents)
288
310
289 if keywords:
311 if keywords:
290 contents = keywords.sub("$\\1$", contents)
312 contents = keywords.sub("$\\1$", contents)
291 if mode == "l" and contents.endswith("\n"):
313 if mode == "l" and contents.endswith("\n"):
292 contents = contents[:-1]
314 contents = contents[:-1]
293
315
294 return contents, mode
316 return contents, mode
295
317
296 def getchanges(self, rev, full):
318 def getchanges(self, rev, full):
297 if full:
319 if full:
298 raise error.Abort(_("convert from p4 does not support --full"))
320 raise error.Abort(_("convert from p4 does not support --full"))
299 return self.files[rev], self.copies[rev], set()
321 return self.files[rev], self.copies[rev], set()
300
322
301 def _construct_commit(self, obj, parents=None):
323 def _construct_commit(self, obj, parents=None):
302 """
324 """
303 Constructs a common.commit object from an unmarshalled
325 Constructs a common.commit object from an unmarshalled
304 `p4 describe` output
326 `p4 describe` output
305 """
327 """
306 desc = self.recode(obj.get("desc", ""))
328 desc = self.recode(obj.get("desc", ""))
307 date = (int(obj["time"]), 0) # timezone not set
329 date = (int(obj["time"]), 0) # timezone not set
308 if parents is None:
330 if parents is None:
309 parents = []
331 parents = []
310
332
311 return common.commit(author=self.recode(obj["user"]),
333 return common.commit(author=self.recode(obj["user"]),
312 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
334 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
313 parents=parents, desc=desc, branch=None, rev=obj['change'],
335 parents=parents, desc=desc, branch=None, rev=obj['change'],
314 extra={"p4": obj['change'], "convert_revision": obj['change']})
336 extra={"p4": obj['change'], "convert_revision": obj['change']})
315
337
316 def _fetch_revision(self, rev):
338 def _fetch_revision(self, rev):
317 """Return an output of `p4 describe` including author, commit date as
339 """Return an output of `p4 describe` including author, commit date as
318 a dictionary."""
340 a dictionary."""
319 cmd = "p4 -G describe -s %s" % rev
341 cmd = "p4 -G describe -s %s" % rev
320 stdout = util.popen(cmd, mode='rb')
342 stdout = util.popen(cmd, mode='rb')
321 return marshal.load(stdout)
343 return marshal.load(stdout)
322
344
323 def getcommit(self, rev):
345 def getcommit(self, rev):
324 if rev in self.changeset:
346 if rev in self.changeset:
325 return self.changeset[rev]
347 return self.changeset[rev]
326 elif rev in self.revmap:
348 elif rev in self.revmap:
327 d = self._fetch_revision(rev)
349 d = self._fetch_revision(rev)
328 return self._construct_commit(d, parents=None)
350 return self._construct_commit(d, parents=None)
329 raise error.Abort(
351 raise error.Abort(
330 _("cannot find %s in the revmap or parsed changesets") % rev)
352 _("cannot find %s in the revmap or parsed changesets") % rev)
331
353
332 def gettags(self):
354 def gettags(self):
333 return {}
355 return {}
334
356
335 def getchangedfiles(self, rev, i):
357 def getchangedfiles(self, rev, i):
336 return sorted([x[0] for x in self.files[rev]])
358 return sorted([x[0] for x in self.files[rev]])
General Comments 0
You need to be logged in to leave comments. Login now