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