##// END OF EJS Templates
convcmd: use our shlex wrapper to avoid Python 3 tracebacks...
Augie Fackler -
r36576:d4c98b67 default
parent child Browse files
Show More
@@ -1,618 +1,615 b''
1 1 # convcmd - convert extension commands definition
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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 collections
10 10 import os
11 import shlex
12 11 import shutil
13 12
14 13 from mercurial.i18n import _
15 14 from mercurial import (
16 15 encoding,
17 16 error,
18 17 hg,
19 18 pycompat,
20 19 scmutil,
21 20 util,
22 21 )
23 22
24 23 from . import (
25 24 bzr,
26 25 common,
27 26 cvs,
28 27 darcs,
29 28 filemap,
30 29 git,
31 30 gnuarch,
32 31 hg as hgconvert,
33 32 monotone,
34 33 p4,
35 34 subversion,
36 35 )
37 36
38 37 mapfile = common.mapfile
39 38 MissingTool = common.MissingTool
40 39 NoRepo = common.NoRepo
41 40 SKIPREV = common.SKIPREV
42 41
43 42 bzr_source = bzr.bzr_source
44 43 convert_cvs = cvs.convert_cvs
45 44 convert_git = git.convert_git
46 45 darcs_source = darcs.darcs_source
47 46 gnuarch_source = gnuarch.gnuarch_source
48 47 mercurial_sink = hgconvert.mercurial_sink
49 48 mercurial_source = hgconvert.mercurial_source
50 49 monotone_source = monotone.monotone_source
51 50 p4_source = p4.p4_source
52 51 svn_sink = subversion.svn_sink
53 52 svn_source = subversion.svn_source
54 53
55 54 orig_encoding = 'ascii'
56 55
57 56 def recode(s):
58 57 if isinstance(s, unicode):
59 58 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 59 else:
61 60 return s.decode('utf-8').encode(
62 61 pycompat.sysstr(orig_encoding), 'replace')
63 62
64 63 def mapbranch(branch, branchmap):
65 64 '''
66 65 >>> bmap = {b'default': b'branch1'}
67 66 >>> for i in [b'', None]:
68 67 ... mapbranch(i, bmap)
69 68 'branch1'
70 69 'branch1'
71 70 >>> bmap = {b'None': b'branch2'}
72 71 >>> for i in [b'', None]:
73 72 ... mapbranch(i, bmap)
74 73 'branch2'
75 74 'branch2'
76 75 >>> bmap = {b'None': b'branch3', b'default': b'branch4'}
77 76 >>> for i in [b'None', b'', None, b'default', b'branch5']:
78 77 ... mapbranch(i, bmap)
79 78 'branch3'
80 79 'branch4'
81 80 'branch4'
82 81 'branch4'
83 82 'branch5'
84 83 '''
85 84 # If branch is None or empty, this commit is coming from the source
86 85 # repository's default branch and destined for the default branch in the
87 86 # destination repository. For such commits, using a literal "default"
88 87 # in branchmap below allows the user to map "default" to an alternate
89 88 # default branch in the destination repository.
90 89 branch = branchmap.get(branch or 'default', branch)
91 90 # At some point we used "None" literal to denote the default branch,
92 91 # attempt to use that for backward compatibility.
93 92 if (not branch):
94 93 branch = branchmap.get('None', branch)
95 94 return branch
96 95
97 96 source_converters = [
98 97 ('cvs', convert_cvs, 'branchsort'),
99 98 ('git', convert_git, 'branchsort'),
100 99 ('svn', svn_source, 'branchsort'),
101 100 ('hg', mercurial_source, 'sourcesort'),
102 101 ('darcs', darcs_source, 'branchsort'),
103 102 ('mtn', monotone_source, 'branchsort'),
104 103 ('gnuarch', gnuarch_source, 'branchsort'),
105 104 ('bzr', bzr_source, 'branchsort'),
106 105 ('p4', p4_source, 'branchsort'),
107 106 ]
108 107
109 108 sink_converters = [
110 109 ('hg', mercurial_sink),
111 110 ('svn', svn_sink),
112 111 ]
113 112
114 113 def convertsource(ui, path, type, revs):
115 114 exceptions = []
116 115 if type and type not in [s[0] for s in source_converters]:
117 116 raise error.Abort(_('%s: invalid source repository type') % type)
118 117 for name, source, sortmode in source_converters:
119 118 try:
120 119 if not type or name == type:
121 120 return source(ui, name, path, revs), sortmode
122 121 except (NoRepo, MissingTool) as inst:
123 122 exceptions.append(inst)
124 123 if not ui.quiet:
125 124 for inst in exceptions:
126 125 ui.write("%s\n" % inst)
127 126 raise error.Abort(_('%s: missing or unsupported repository') % path)
128 127
129 128 def convertsink(ui, path, type):
130 129 if type and type not in [s[0] for s in sink_converters]:
131 130 raise error.Abort(_('%s: invalid destination repository type') % type)
132 131 for name, sink in sink_converters:
133 132 try:
134 133 if not type or name == type:
135 134 return sink(ui, name, path)
136 135 except NoRepo as inst:
137 136 ui.note(_("convert: %s\n") % inst)
138 137 except MissingTool as inst:
139 138 raise error.Abort('%s\n' % inst)
140 139 raise error.Abort(_('%s: unknown repository type') % path)
141 140
142 141 class progresssource(object):
143 142 def __init__(self, ui, source, filecount):
144 143 self.ui = ui
145 144 self.source = source
146 145 self.filecount = filecount
147 146 self.retrieved = 0
148 147
149 148 def getfile(self, file, rev):
150 149 self.retrieved += 1
151 150 self.ui.progress(_('getting files'), self.retrieved,
152 151 item=file, total=self.filecount, unit=_('files'))
153 152 return self.source.getfile(file, rev)
154 153
155 154 def targetfilebelongstosource(self, targetfilename):
156 155 return self.source.targetfilebelongstosource(targetfilename)
157 156
158 157 def lookuprev(self, rev):
159 158 return self.source.lookuprev(rev)
160 159
161 160 def close(self):
162 161 self.ui.progress(_('getting files'), None)
163 162
164 163 class converter(object):
165 164 def __init__(self, ui, source, dest, revmapfile, opts):
166 165
167 166 self.source = source
168 167 self.dest = dest
169 168 self.ui = ui
170 169 self.opts = opts
171 170 self.commitcache = {}
172 171 self.authors = {}
173 172 self.authorfile = None
174 173
175 174 # Record converted revisions persistently: maps source revision
176 175 # ID to target revision ID (both strings). (This is how
177 176 # incremental conversions work.)
178 177 self.map = mapfile(ui, revmapfile)
179 178
180 179 # Read first the dst author map if any
181 180 authorfile = self.dest.authorfile()
182 181 if authorfile and os.path.exists(authorfile):
183 182 self.readauthormap(authorfile)
184 183 # Extend/Override with new author map if necessary
185 184 if opts.get('authormap'):
186 185 self.readauthormap(opts.get('authormap'))
187 186 self.authorfile = self.dest.authorfile()
188 187
189 188 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
190 189 self.branchmap = mapfile(ui, opts.get('branchmap'))
191 190
192 191 def parsesplicemap(self, path):
193 192 """ check and validate the splicemap format and
194 193 return a child/parents dictionary.
195 194 Format checking has two parts.
196 195 1. generic format which is same across all source types
197 196 2. specific format checking which may be different for
198 197 different source type. This logic is implemented in
199 198 checkrevformat function in source files like
200 199 hg.py, subversion.py etc.
201 200 """
202 201
203 202 if not path:
204 203 return {}
205 204 m = {}
206 205 try:
207 206 fp = open(path, 'rb')
208 207 for i, line in enumerate(util.iterfile(fp)):
209 208 line = line.splitlines()[0].rstrip()
210 209 if not line:
211 210 # Ignore blank lines
212 211 continue
213 212 # split line
214 lex = shlex.shlex(line, posix=True)
215 lex.whitespace_split = True
216 lex.whitespace += ','
213 lex = common.shlexer(data=line, whitespace=',')
217 214 line = list(lex)
218 215 # check number of parents
219 216 if not (2 <= len(line) <= 3):
220 217 raise error.Abort(_('syntax error in %s(%d): child parent1'
221 218 '[,parent2] expected') % (path, i + 1))
222 219 for part in line:
223 220 self.source.checkrevformat(part)
224 221 child, p1, p2 = line[0], line[1:2], line[2:]
225 222 if p1 == p2:
226 223 m[child] = p1
227 224 else:
228 225 m[child] = p1 + p2
229 226 # if file does not exist or error reading, exit
230 227 except IOError:
231 228 raise error.Abort(_('splicemap file not found or error reading %s:')
232 229 % path)
233 230 return m
234 231
235 232
236 233 def walktree(self, heads):
237 234 '''Return a mapping that identifies the uncommitted parents of every
238 235 uncommitted changeset.'''
239 236 visit = heads
240 237 known = set()
241 238 parents = {}
242 239 numcommits = self.source.numcommits()
243 240 while visit:
244 241 n = visit.pop(0)
245 242 if n in known:
246 243 continue
247 244 if n in self.map:
248 245 m = self.map[n]
249 246 if m == SKIPREV or self.dest.hascommitfrommap(m):
250 247 continue
251 248 known.add(n)
252 249 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
253 250 total=numcommits)
254 251 commit = self.cachecommit(n)
255 252 parents[n] = []
256 253 for p in commit.parents:
257 254 parents[n].append(p)
258 255 visit.append(p)
259 256 self.ui.progress(_('scanning'), None)
260 257
261 258 return parents
262 259
263 260 def mergesplicemap(self, parents, splicemap):
264 261 """A splicemap redefines child/parent relationships. Check the
265 262 map contains valid revision identifiers and merge the new
266 263 links in the source graph.
267 264 """
268 265 for c in sorted(splicemap):
269 266 if c not in parents:
270 267 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
271 268 # Could be in source but not converted during this run
272 269 self.ui.warn(_('splice map revision %s is not being '
273 270 'converted, ignoring\n') % c)
274 271 continue
275 272 pc = []
276 273 for p in splicemap[c]:
277 274 # We do not have to wait for nodes already in dest.
278 275 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
279 276 continue
280 277 # Parent is not in dest and not being converted, not good
281 278 if p not in parents:
282 279 raise error.Abort(_('unknown splice map parent: %s') % p)
283 280 pc.append(p)
284 281 parents[c] = pc
285 282
286 283 def toposort(self, parents, sortmode):
287 284 '''Return an ordering such that every uncommitted changeset is
288 285 preceded by all its uncommitted ancestors.'''
289 286
290 287 def mapchildren(parents):
291 288 """Return a (children, roots) tuple where 'children' maps parent
292 289 revision identifiers to children ones, and 'roots' is the list of
293 290 revisions without parents. 'parents' must be a mapping of revision
294 291 identifier to its parents ones.
295 292 """
296 293 visit = collections.deque(sorted(parents))
297 294 seen = set()
298 295 children = {}
299 296 roots = []
300 297
301 298 while visit:
302 299 n = visit.popleft()
303 300 if n in seen:
304 301 continue
305 302 seen.add(n)
306 303 # Ensure that nodes without parents are present in the
307 304 # 'children' mapping.
308 305 children.setdefault(n, [])
309 306 hasparent = False
310 307 for p in parents[n]:
311 308 if p not in self.map:
312 309 visit.append(p)
313 310 hasparent = True
314 311 children.setdefault(p, []).append(n)
315 312 if not hasparent:
316 313 roots.append(n)
317 314
318 315 return children, roots
319 316
320 317 # Sort functions are supposed to take a list of revisions which
321 318 # can be converted immediately and pick one
322 319
323 320 def makebranchsorter():
324 321 """If the previously converted revision has a child in the
325 322 eligible revisions list, pick it. Return the list head
326 323 otherwise. Branch sort attempts to minimize branch
327 324 switching, which is harmful for Mercurial backend
328 325 compression.
329 326 """
330 327 prev = [None]
331 328 def picknext(nodes):
332 329 next = nodes[0]
333 330 for n in nodes:
334 331 if prev[0] in parents[n]:
335 332 next = n
336 333 break
337 334 prev[0] = next
338 335 return next
339 336 return picknext
340 337
341 338 def makesourcesorter():
342 339 """Source specific sort."""
343 340 keyfn = lambda n: self.commitcache[n].sortkey
344 341 def picknext(nodes):
345 342 return sorted(nodes, key=keyfn)[0]
346 343 return picknext
347 344
348 345 def makeclosesorter():
349 346 """Close order sort."""
350 347 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
351 348 self.commitcache[n].sortkey)
352 349 def picknext(nodes):
353 350 return sorted(nodes, key=keyfn)[0]
354 351 return picknext
355 352
356 353 def makedatesorter():
357 354 """Sort revisions by date."""
358 355 dates = {}
359 356 def getdate(n):
360 357 if n not in dates:
361 358 dates[n] = util.parsedate(self.commitcache[n].date)
362 359 return dates[n]
363 360
364 361 def picknext(nodes):
365 362 return min([(getdate(n), n) for n in nodes])[1]
366 363
367 364 return picknext
368 365
369 366 if sortmode == 'branchsort':
370 367 picknext = makebranchsorter()
371 368 elif sortmode == 'datesort':
372 369 picknext = makedatesorter()
373 370 elif sortmode == 'sourcesort':
374 371 picknext = makesourcesorter()
375 372 elif sortmode == 'closesort':
376 373 picknext = makeclosesorter()
377 374 else:
378 375 raise error.Abort(_('unknown sort mode: %s') % sortmode)
379 376
380 377 children, actives = mapchildren(parents)
381 378
382 379 s = []
383 380 pendings = {}
384 381 while actives:
385 382 n = picknext(actives)
386 383 actives.remove(n)
387 384 s.append(n)
388 385
389 386 # Update dependents list
390 387 for c in children.get(n, []):
391 388 if c not in pendings:
392 389 pendings[c] = [p for p in parents[c] if p not in self.map]
393 390 try:
394 391 pendings[c].remove(n)
395 392 except ValueError:
396 393 raise error.Abort(_('cycle detected between %s and %s')
397 394 % (recode(c), recode(n)))
398 395 if not pendings[c]:
399 396 # Parents are converted, node is eligible
400 397 actives.insert(0, c)
401 398 pendings[c] = None
402 399
403 400 if len(s) != len(parents):
404 401 raise error.Abort(_("not all revisions were sorted"))
405 402
406 403 return s
407 404
408 405 def writeauthormap(self):
409 406 authorfile = self.authorfile
410 407 if authorfile:
411 408 self.ui.status(_('writing author map file %s\n') % authorfile)
412 409 ofile = open(authorfile, 'wb+')
413 410 for author in self.authors:
414 411 ofile.write(util.tonativeeol("%s=%s\n"
415 412 % (author, self.authors[author])))
416 413 ofile.close()
417 414
418 415 def readauthormap(self, authorfile):
419 416 afile = open(authorfile, 'rb')
420 417 for line in afile:
421 418
422 419 line = line.strip()
423 420 if not line or line.startswith('#'):
424 421 continue
425 422
426 423 try:
427 424 srcauthor, dstauthor = line.split('=', 1)
428 425 except ValueError:
429 426 msg = _('ignoring bad line in author map file %s: %s\n')
430 427 self.ui.warn(msg % (authorfile, line.rstrip()))
431 428 continue
432 429
433 430 srcauthor = srcauthor.strip()
434 431 dstauthor = dstauthor.strip()
435 432 if self.authors.get(srcauthor) in (None, dstauthor):
436 433 msg = _('mapping author %s to %s\n')
437 434 self.ui.debug(msg % (srcauthor, dstauthor))
438 435 self.authors[srcauthor] = dstauthor
439 436 continue
440 437
441 438 m = _('overriding mapping for author %s, was %s, will be %s\n')
442 439 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
443 440
444 441 afile.close()
445 442
446 443 def cachecommit(self, rev):
447 444 commit = self.source.getcommit(rev)
448 445 commit.author = self.authors.get(commit.author, commit.author)
449 446 commit.branch = mapbranch(commit.branch, self.branchmap)
450 447 self.commitcache[rev] = commit
451 448 return commit
452 449
453 450 def copy(self, rev):
454 451 commit = self.commitcache[rev]
455 452 full = self.opts.get('full')
456 453 changes = self.source.getchanges(rev, full)
457 454 if isinstance(changes, bytes):
458 455 if changes == SKIPREV:
459 456 dest = SKIPREV
460 457 else:
461 458 dest = self.map[changes]
462 459 self.map[rev] = dest
463 460 return
464 461 files, copies, cleanp2 = changes
465 462 pbranches = []
466 463 if commit.parents:
467 464 for prev in commit.parents:
468 465 if prev not in self.commitcache:
469 466 self.cachecommit(prev)
470 467 pbranches.append((self.map[prev],
471 468 self.commitcache[prev].branch))
472 469 self.dest.setbranch(commit.branch, pbranches)
473 470 try:
474 471 parents = self.splicemap[rev]
475 472 self.ui.status(_('spliced in %s as parents of %s\n') %
476 473 (_(' and ').join(parents), rev))
477 474 parents = [self.map.get(p, p) for p in parents]
478 475 except KeyError:
479 476 parents = [b[0] for b in pbranches]
480 477 parents.extend(self.map[x]
481 478 for x in commit.optparents
482 479 if x in self.map)
483 480 if len(pbranches) != 2:
484 481 cleanp2 = set()
485 482 if len(parents) < 3:
486 483 source = progresssource(self.ui, self.source, len(files))
487 484 else:
488 485 # For an octopus merge, we end up traversing the list of
489 486 # changed files N-1 times. This tweak to the number of
490 487 # files makes it so the progress bar doesn't overflow
491 488 # itself.
492 489 source = progresssource(self.ui, self.source,
493 490 len(files) * (len(parents) - 1))
494 491 newnode = self.dest.putcommit(files, copies, parents, commit,
495 492 source, self.map, full, cleanp2)
496 493 source.close()
497 494 self.source.converted(rev, newnode)
498 495 self.map[rev] = newnode
499 496
500 497 def convert(self, sortmode):
501 498 try:
502 499 self.source.before()
503 500 self.dest.before()
504 501 self.source.setrevmap(self.map)
505 502 self.ui.status(_("scanning source...\n"))
506 503 heads = self.source.getheads()
507 504 parents = self.walktree(heads)
508 505 self.mergesplicemap(parents, self.splicemap)
509 506 self.ui.status(_("sorting...\n"))
510 507 t = self.toposort(parents, sortmode)
511 508 num = len(t)
512 509 c = None
513 510
514 511 self.ui.status(_("converting...\n"))
515 512 for i, c in enumerate(t):
516 513 num -= 1
517 514 desc = self.commitcache[c].desc
518 515 if "\n" in desc:
519 516 desc = desc.splitlines()[0]
520 517 # convert log message to local encoding without using
521 518 # tolocal() because the encoding.encoding convert()
522 519 # uses is 'utf-8'
523 520 self.ui.status("%d %s\n" % (num, recode(desc)))
524 521 self.ui.note(_("source: %s\n") % recode(c))
525 522 self.ui.progress(_('converting'), i, unit=_('revisions'),
526 523 total=len(t))
527 524 self.copy(c)
528 525 self.ui.progress(_('converting'), None)
529 526
530 527 if not self.ui.configbool('convert', 'skiptags'):
531 528 tags = self.source.gettags()
532 529 ctags = {}
533 530 for k in tags:
534 531 v = tags[k]
535 532 if self.map.get(v, SKIPREV) != SKIPREV:
536 533 ctags[k] = self.map[v]
537 534
538 535 if c and ctags:
539 536 nrev, tagsparent = self.dest.puttags(ctags)
540 537 if nrev and tagsparent:
541 538 # write another hash correspondence to override the
542 539 # previous one so we don't end up with extra tag heads
543 540 tagsparents = [e for e in self.map.iteritems()
544 541 if e[1] == tagsparent]
545 542 if tagsparents:
546 543 self.map[tagsparents[0][0]] = nrev
547 544
548 545 bookmarks = self.source.getbookmarks()
549 546 cbookmarks = {}
550 547 for k in bookmarks:
551 548 v = bookmarks[k]
552 549 if self.map.get(v, SKIPREV) != SKIPREV:
553 550 cbookmarks[k] = self.map[v]
554 551
555 552 if c and cbookmarks:
556 553 self.dest.putbookmarks(cbookmarks)
557 554
558 555 self.writeauthormap()
559 556 finally:
560 557 self.cleanup()
561 558
562 559 def cleanup(self):
563 560 try:
564 561 self.dest.after()
565 562 finally:
566 563 self.source.after()
567 564 self.map.close()
568 565
569 566 def convert(ui, src, dest=None, revmapfile=None, **opts):
570 567 opts = pycompat.byteskwargs(opts)
571 568 global orig_encoding
572 569 orig_encoding = encoding.encoding
573 570 encoding.encoding = 'UTF-8'
574 571
575 572 # support --authors as an alias for --authormap
576 573 if not opts.get('authormap'):
577 574 opts['authormap'] = opts.get('authors')
578 575
579 576 if not dest:
580 577 dest = hg.defaultdest(src) + "-hg"
581 578 ui.status(_("assuming destination %s\n") % dest)
582 579
583 580 destc = convertsink(ui, dest, opts.get('dest_type'))
584 581 destc = scmutil.wrapconvertsink(destc)
585 582
586 583 try:
587 584 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
588 585 opts.get('rev'))
589 586 except Exception:
590 587 for path in destc.created:
591 588 shutil.rmtree(path, True)
592 589 raise
593 590
594 591 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
595 592 sortmode = [m for m in sortmodes if opts.get(m)]
596 593 if len(sortmode) > 1:
597 594 raise error.Abort(_('more than one sort mode specified'))
598 595 if sortmode:
599 596 sortmode = sortmode[0]
600 597 else:
601 598 sortmode = defaultsort
602 599
603 600 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
604 601 raise error.Abort(_('--sourcesort is not supported by this data source')
605 602 )
606 603 if sortmode == 'closesort' and not srcc.hasnativeclose():
607 604 raise error.Abort(_('--closesort is not supported by this data source'))
608 605
609 606 fmap = opts.get('filemap')
610 607 if fmap:
611 608 srcc = filemap.filemap_source(ui, srcc, fmap)
612 609 destc.setfilemapmode(True)
613 610
614 611 if not revmapfile:
615 612 revmapfile = destc.revmapfile()
616 613
617 614 c = converter(ui, srcc, destc, revmapfile, opts)
618 615 c.convert(sortmode)
General Comments 0
You need to be logged in to leave comments. Login now