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