##// END OF EJS Templates
convert: return commit objects for revisions in the revmap...
David Soria Parra -
r30604:b654112a default
parent child Browse files
Show More
@@ -1,330 +1,336 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 if lastid:
154 if lastid:
155 parents = [lastid]
155 parents = [lastid]
156 else:
156 else:
157 parents = []
157 parents = []
158
158
159 d = self._fetch_revision(change)
159 d = self._fetch_revision(change)
160 c = self._construct_commit(d, parents)
160 c = self._construct_commit(d, parents)
161
161
162 shortdesc = c.desc.splitlines(True)[0].rstrip('\r\n')
162 shortdesc = c.desc.splitlines(True)[0].rstrip('\r\n')
163 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
163 t = '%s %s' % (c.rev, repr(shortdesc)[1:-1])
164 ui.status(util.ellipsis(t, 80) + '\n')
164 ui.status(util.ellipsis(t, 80) + '\n')
165
165
166 files = []
166 files = []
167 copies = {}
167 copies = {}
168 copiedfiles = []
168 copiedfiles = []
169 i = 0
169 i = 0
170 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:
171 oldname = d["depotFile%d" % i]
171 oldname = d["depotFile%d" % i]
172 filename = None
172 filename = None
173 for v in vieworder:
173 for v in vieworder:
174 if oldname.lower().startswith(v.lower()):
174 if oldname.lower().startswith(v.lower()):
175 filename = decodefilename(views[v] + oldname[len(v):])
175 filename = decodefilename(views[v] + oldname[len(v):])
176 break
176 break
177 if filename:
177 if filename:
178 files.append((filename, d["rev%d" % i]))
178 files.append((filename, d["rev%d" % i]))
179 self.depotname[filename] = oldname
179 self.depotname[filename] = oldname
180 if (d.get("action%d" % i) == "move/add"):
180 if (d.get("action%d" % i) == "move/add"):
181 copiedfiles.append(filename)
181 copiedfiles.append(filename)
182 self.localname[oldname] = filename
182 self.localname[oldname] = filename
183 i += 1
183 i += 1
184
184
185 # Collect information about copied files
185 # Collect information about copied files
186 for filename in copiedfiles:
186 for filename in copiedfiles:
187 oldname = self.depotname[filename]
187 oldname = self.depotname[filename]
188
188
189 flcmd = 'p4 -G filelog %s' \
189 flcmd = 'p4 -G filelog %s' \
190 % util.shellquote(oldname)
190 % util.shellquote(oldname)
191 flstdout = util.popen(flcmd, mode='rb')
191 flstdout = util.popen(flcmd, mode='rb')
192
192
193 copiedfilename = None
193 copiedfilename = None
194 for d in loaditer(flstdout):
194 for d in loaditer(flstdout):
195 copiedoldname = None
195 copiedoldname = None
196
196
197 i = 0
197 i = 0
198 while ("change%d" % i) in d:
198 while ("change%d" % i) in d:
199 if (d["change%d" % i] == change and
199 if (d["change%d" % i] == change and
200 d["action%d" % i] == "move/add"):
200 d["action%d" % i] == "move/add"):
201 j = 0
201 j = 0
202 while ("file%d,%d" % (i, j)) in d:
202 while ("file%d,%d" % (i, j)) in d:
203 if d["how%d,%d" % (i, j)] == "moved from":
203 if d["how%d,%d" % (i, j)] == "moved from":
204 copiedoldname = d["file%d,%d" % (i, j)]
204 copiedoldname = d["file%d,%d" % (i, j)]
205 break
205 break
206 j += 1
206 j += 1
207 i += 1
207 i += 1
208
208
209 if copiedoldname and copiedoldname in self.localname:
209 if copiedoldname and copiedoldname in self.localname:
210 copiedfilename = self.localname[copiedoldname]
210 copiedfilename = self.localname[copiedoldname]
211 break
211 break
212
212
213 if copiedfilename:
213 if copiedfilename:
214 copies[filename] = copiedfilename
214 copies[filename] = copiedfilename
215 else:
215 else:
216 ui.warn(_("cannot find source for copied file: %s@%s\n")
216 ui.warn(_("cannot find source for copied file: %s@%s\n")
217 % (filename, change))
217 % (filename, change))
218
218
219 self.changeset[change] = c
219 self.changeset[change] = c
220 self.files[change] = files
220 self.files[change] = files
221 self.copies[change] = copies
221 self.copies[change] = copies
222 lastid = change
222 lastid = change
223
223
224 if lastid and len(self.changeset) > 0:
224 if lastid and len(self.changeset) > 0:
225 self.heads = [lastid]
225 self.heads = [lastid]
226
226
227 def getheads(self):
227 def getheads(self):
228 return self.heads
228 return self.heads
229
229
230 def getfile(self, name, rev):
230 def getfile(self, name, rev):
231 cmd = 'p4 -G print %s' \
231 cmd = 'p4 -G print %s' \
232 % util.shellquote("%s#%s" % (self.depotname[name], rev))
232 % util.shellquote("%s#%s" % (self.depotname[name], rev))
233
233
234 lasterror = None
234 lasterror = None
235 while True:
235 while True:
236 stdout = util.popen(cmd, mode='rb')
236 stdout = util.popen(cmd, mode='rb')
237
237
238 mode = None
238 mode = None
239 contents = []
239 contents = []
240 keywords = None
240 keywords = None
241
241
242 for d in loaditer(stdout):
242 for d in loaditer(stdout):
243 code = d["code"]
243 code = d["code"]
244 data = d.get("data")
244 data = d.get("data")
245
245
246 if code == "error":
246 if code == "error":
247 # if this is the first time error happened
247 # if this is the first time error happened
248 # re-attempt getting the file
248 # re-attempt getting the file
249 if not lasterror:
249 if not lasterror:
250 lasterror = IOError(d["generic"], data)
250 lasterror = IOError(d["generic"], data)
251 # this will exit inner-most for-loop
251 # this will exit inner-most for-loop
252 break
252 break
253 else:
253 else:
254 raise lasterror
254 raise lasterror
255
255
256 elif code == "stat":
256 elif code == "stat":
257 action = d.get("action")
257 action = d.get("action")
258 if action in ["purge", "delete", "move/delete"]:
258 if action in ["purge", "delete", "move/delete"]:
259 return None, None
259 return None, None
260 p4type = self.re_type.match(d["type"])
260 p4type = self.re_type.match(d["type"])
261 if p4type:
261 if p4type:
262 mode = ""
262 mode = ""
263 flags = ((p4type.group(1) or "")
263 flags = ((p4type.group(1) or "")
264 + (p4type.group(3) or ""))
264 + (p4type.group(3) or ""))
265 if "x" in flags:
265 if "x" in flags:
266 mode = "x"
266 mode = "x"
267 if p4type.group(2) == "symlink":
267 if p4type.group(2) == "symlink":
268 mode = "l"
268 mode = "l"
269 if "ko" in flags:
269 if "ko" in flags:
270 keywords = self.re_keywords_old
270 keywords = self.re_keywords_old
271 elif "k" in flags:
271 elif "k" in flags:
272 keywords = self.re_keywords
272 keywords = self.re_keywords
273
273
274 elif code == "text" or code == "binary":
274 elif code == "text" or code == "binary":
275 contents.append(data)
275 contents.append(data)
276
276
277 lasterror = None
277 lasterror = None
278
278
279 if not lasterror:
279 if not lasterror:
280 break
280 break
281
281
282 if mode is None:
282 if mode is None:
283 return None, None
283 return None, None
284
284
285 contents = ''.join(contents)
285 contents = ''.join(contents)
286
286
287 if keywords:
287 if keywords:
288 contents = keywords.sub("$\\1$", contents)
288 contents = keywords.sub("$\\1$", contents)
289 if mode == "l" and contents.endswith("\n"):
289 if mode == "l" and contents.endswith("\n"):
290 contents = contents[:-1]
290 contents = contents[:-1]
291
291
292 return contents, mode
292 return contents, mode
293
293
294 def getchanges(self, rev, full):
294 def getchanges(self, rev, full):
295 if full:
295 if full:
296 raise error.Abort(_("convert from p4 does not support --full"))
296 raise error.Abort(_("convert from p4 does not support --full"))
297 return self.files[rev], self.copies[rev], set()
297 return self.files[rev], self.copies[rev], set()
298
298
299 def _construct_commit(self, obj, parents=None):
299 def _construct_commit(self, obj, parents=None):
300 """
300 """
301 Constructs a common.commit object from an unmarshalled
301 Constructs a common.commit object from an unmarshalled
302 `p4 describe` output
302 `p4 describe` output
303 """
303 """
304 desc = self.recode(obj.get("desc", ""))
304 desc = self.recode(obj.get("desc", ""))
305 shortdesc = desc.split("\n", 1)[0]
305 shortdesc = desc.split("\n", 1)[0]
306
306
307 date = (int(obj["time"]), 0) # timezone not set
307 date = (int(obj["time"]), 0) # timezone not set
308 if parents is None:
308 if parents is None:
309 parents = []
309 parents = []
310
310
311 return common.commit(author=self.recode(obj["user"]),
311 return common.commit(author=self.recode(obj["user"]),
312 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
312 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
313 parents=parents, desc=desc, branch=None, rev=obj['change'],
313 parents=parents, desc=desc, branch=None, rev=obj['change'],
314 extra={"p4": obj['change'], "convert_revision": obj['change']})
314 extra={"p4": obj['change'], "convert_revision": obj['change']})
315
315
316 def _fetch_revision(self, rev):
316 def _fetch_revision(self, rev):
317 """Return an output of `p4 describe` including author, commit date as
317 """Return an output of `p4 describe` including author, commit date as
318 a dictionary."""
318 a dictionary."""
319 cmd = "p4 -G describe -s %s" % rev
319 cmd = "p4 -G describe -s %s" % rev
320 stdout = util.popen(cmd, mode='rb')
320 stdout = util.popen(cmd, mode='rb')
321 return marshal.load(stdout)
321 return marshal.load(stdout)
322
322
323 def getcommit(self, rev):
323 def getcommit(self, rev):
324 return self.changeset[rev]
324 if rev in self.changeset:
325 return self.changeset[rev]
326 elif rev in self.revmap:
327 d = self._fetch_revision(rev)
328 return self._construct_commit(d, parents=None)
329 raise error.Abort(
330 _("cannot find %s in the revmap or parsed changesets") % rev)
325
331
326 def gettags(self):
332 def gettags(self):
327 return {}
333 return {}
328
334
329 def getchangedfiles(self, rev, i):
335 def getchangedfiles(self, rev, i):
330 return sorted([x[0] for x in self.files[rev]])
336 return sorted([x[0] for x in self.files[rev]])
General Comments 0
You need to be logged in to leave comments. Login now