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