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