##// END OF EJS Templates
convert/subversion: fix default URL checker prototype
Patrick Mezard -
r10885:9e4d120e stable
parent child Browse files
Show More
@@ -1,1171 +1,1171 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4
5 5 import os
6 6 import re
7 7 import sys
8 8 import cPickle as pickle
9 9 import tempfile
10 10 import urllib
11 11 import urllib2
12 12
13 13 from mercurial import strutil, util, encoding
14 14 from mercurial.i18n import _
15 15
16 16 # Subversion stuff. Works best with very recent Python SVN bindings
17 17 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
18 18 # these bindings.
19 19
20 20 from cStringIO import StringIO
21 21
22 22 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
23 23 from common import commandline, converter_source, converter_sink, mapfile
24 24
25 25 try:
26 26 from svn.core import SubversionException, Pool
27 27 import svn
28 28 import svn.client
29 29 import svn.core
30 30 import svn.ra
31 31 import svn.delta
32 32 import transport
33 33 import warnings
34 34 warnings.filterwarnings('ignore',
35 35 module='svn.core',
36 36 category=DeprecationWarning)
37 37
38 38 except ImportError:
39 39 pass
40 40
41 41 class SvnPathNotFound(Exception):
42 42 pass
43 43
44 44 def geturl(path):
45 45 try:
46 46 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
47 47 except SubversionException:
48 48 pass
49 49 if os.path.isdir(path):
50 50 path = os.path.normpath(os.path.abspath(path))
51 51 if os.name == 'nt':
52 52 path = '/' + util.normpath(path)
53 53 # Module URL is later compared with the repository URL returned
54 54 # by svn API, which is UTF-8.
55 55 path = encoding.tolocal(path)
56 56 return 'file://%s' % urllib.quote(path)
57 57 return path
58 58
59 59 def optrev(number):
60 60 optrev = svn.core.svn_opt_revision_t()
61 61 optrev.kind = svn.core.svn_opt_revision_number
62 62 optrev.value.number = number
63 63 return optrev
64 64
65 65 class changedpath(object):
66 66 def __init__(self, p):
67 67 self.copyfrom_path = p.copyfrom_path
68 68 self.copyfrom_rev = p.copyfrom_rev
69 69 self.action = p.action
70 70
71 71 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 72 strict_node_history=False):
73 73 protocol = -1
74 74 def receiver(orig_paths, revnum, author, date, message, pool):
75 75 if orig_paths is not None:
76 76 for k, v in orig_paths.iteritems():
77 77 orig_paths[k] = changedpath(v)
78 78 pickle.dump((orig_paths, revnum, author, date, message),
79 79 fp, protocol)
80 80
81 81 try:
82 82 # Use an ra of our own so that our parent can consume
83 83 # our results without confusing the server.
84 84 t = transport.SvnRaTransport(url=url)
85 85 svn.ra.get_log(t.ra, paths, start, end, limit,
86 86 discover_changed_paths,
87 87 strict_node_history,
88 88 receiver)
89 89 except SubversionException, (inst, num):
90 90 pickle.dump(num, fp, protocol)
91 91 except IOError:
92 92 # Caller may interrupt the iteration
93 93 pickle.dump(None, fp, protocol)
94 94 else:
95 95 pickle.dump(None, fp, protocol)
96 96 fp.close()
97 97 # With large history, cleanup process goes crazy and suddenly
98 98 # consumes *huge* amount of memory. The output file being closed,
99 99 # there is no need for clean termination.
100 100 os._exit(0)
101 101
102 102 def debugsvnlog(ui, **opts):
103 103 """Fetch SVN log in a subprocess and channel them back to parent to
104 104 avoid memory collection issues.
105 105 """
106 106 util.set_binary(sys.stdin)
107 107 util.set_binary(sys.stdout)
108 108 args = decodeargs(sys.stdin.read())
109 109 get_log_child(sys.stdout, *args)
110 110
111 111 class logstream(object):
112 112 """Interruptible revision log iterator."""
113 113 def __init__(self, stdout):
114 114 self._stdout = stdout
115 115
116 116 def __iter__(self):
117 117 while True:
118 118 try:
119 119 entry = pickle.load(self._stdout)
120 120 except EOFError:
121 121 raise util.Abort(_('Mercurial failed to run itself, check'
122 122 ' hg executable is in PATH'))
123 123 try:
124 124 orig_paths, revnum, author, date, message = entry
125 125 except:
126 126 if entry is None:
127 127 break
128 128 raise SubversionException("child raised exception", entry)
129 129 yield entry
130 130
131 131 def close(self):
132 132 if self._stdout:
133 133 self._stdout.close()
134 134 self._stdout = None
135 135
136 136
137 137 # Check to see if the given path is a local Subversion repo. Verify this by
138 138 # looking for several svn-specific files and directories in the given
139 139 # directory.
140 140 def filecheck(ui, path, proto):
141 141 for x in ('locks', 'hooks', 'format', 'db'):
142 142 if not os.path.exists(os.path.join(path, x)):
143 143 return False
144 144 return True
145 145
146 146 # Check to see if a given path is the root of an svn repo over http. We verify
147 147 # this by requesting a version-controlled URL we know can't exist and looking
148 148 # for the svn-specific "not found" XML.
149 149 def httpcheck(ui, path, proto):
150 150 try:
151 151 opener = urllib2.build_opener()
152 152 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
153 153 data = rsp.read()
154 154 except urllib2.HTTPError, inst:
155 155 if inst.code != 404:
156 156 # Except for 404 we cannot know for sure this is not an svn repo
157 157 ui.warn(_('svn: cannot probe remote repository, assume it could '
158 158 'be a subversion repository. Use --source-type if you '
159 159 'know better.\n'))
160 160 return True
161 161 data = inst.fp.read()
162 162 except:
163 163 # Could be urllib2.URLError if the URL is invalid or anything else.
164 164 return False
165 165 return '<m:human-readable errcode="160013">' in data
166 166
167 167 protomap = {'http': httpcheck,
168 168 'https': httpcheck,
169 169 'file': filecheck,
170 170 }
171 171 def issvnurl(ui, url):
172 172 try:
173 173 proto, path = url.split('://', 1)
174 174 if proto == 'file':
175 175 path = urllib.url2pathname(path)
176 176 except ValueError:
177 177 proto = 'file'
178 178 path = os.path.abspath(url)
179 179 if proto == 'file':
180 180 path = path.replace(os.sep, '/')
181 check = protomap.get(proto, lambda p, p2: False)
181 check = protomap.get(proto, lambda *args: False)
182 182 while '/' in path:
183 183 if check(ui, path, proto):
184 184 return True
185 185 path = path.rsplit('/', 1)[0]
186 186 return False
187 187
188 188 # SVN conversion code stolen from bzr-svn and tailor
189 189 #
190 190 # Subversion looks like a versioned filesystem, branches structures
191 191 # are defined by conventions and not enforced by the tool. First,
192 192 # we define the potential branches (modules) as "trunk" and "branches"
193 193 # children directories. Revisions are then identified by their
194 194 # module and revision number (and a repository identifier).
195 195 #
196 196 # The revision graph is really a tree (or a forest). By default, a
197 197 # revision parent is the previous revision in the same module. If the
198 198 # module directory is copied/moved from another module then the
199 199 # revision is the module root and its parent the source revision in
200 200 # the parent module. A revision has at most one parent.
201 201 #
202 202 class svn_source(converter_source):
203 203 def __init__(self, ui, url, rev=None):
204 204 super(svn_source, self).__init__(ui, url, rev=rev)
205 205
206 206 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
207 207 (os.path.exists(url) and
208 208 os.path.exists(os.path.join(url, '.svn'))) or
209 209 issvnurl(ui, url)):
210 210 raise NoRepo("%s does not look like a Subversion repo" % url)
211 211
212 212 try:
213 213 SubversionException
214 214 except NameError:
215 215 raise MissingTool(_('Subversion python bindings could not be loaded'))
216 216
217 217 try:
218 218 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
219 219 if version < (1, 4):
220 220 raise MissingTool(_('Subversion python bindings %d.%d found, '
221 221 '1.4 or later required') % version)
222 222 except AttributeError:
223 223 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
224 224 'or later required'))
225 225
226 226 self.lastrevs = {}
227 227
228 228 latest = None
229 229 try:
230 230 # Support file://path@rev syntax. Useful e.g. to convert
231 231 # deleted branches.
232 232 at = url.rfind('@')
233 233 if at >= 0:
234 234 latest = int(url[at + 1:])
235 235 url = url[:at]
236 236 except ValueError:
237 237 pass
238 238 self.url = geturl(url)
239 239 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
240 240 try:
241 241 self.transport = transport.SvnRaTransport(url=self.url)
242 242 self.ra = self.transport.ra
243 243 self.ctx = self.transport.client
244 244 self.baseurl = svn.ra.get_repos_root(self.ra)
245 245 # Module is either empty or a repository path starting with
246 246 # a slash and not ending with a slash.
247 247 self.module = urllib.unquote(self.url[len(self.baseurl):])
248 248 self.prevmodule = None
249 249 self.rootmodule = self.module
250 250 self.commits = {}
251 251 self.paths = {}
252 252 self.uuid = svn.ra.get_uuid(self.ra)
253 253 except SubversionException:
254 254 ui.traceback()
255 255 raise NoRepo("%s does not look like a Subversion repo" % self.url)
256 256
257 257 if rev:
258 258 try:
259 259 latest = int(rev)
260 260 except ValueError:
261 261 raise util.Abort(_('svn: revision %s is not an integer') % rev)
262 262
263 263 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
264 264 try:
265 265 self.startrev = int(self.startrev)
266 266 if self.startrev < 0:
267 267 self.startrev = 0
268 268 except ValueError:
269 269 raise util.Abort(_('svn: start revision %s is not an integer')
270 270 % self.startrev)
271 271
272 272 self.head = self.latest(self.module, latest)
273 273 if not self.head:
274 274 raise util.Abort(_('no revision found in module %s')
275 275 % self.module)
276 276 self.last_changed = self.revnum(self.head)
277 277
278 278 self._changescache = None
279 279
280 280 if os.path.exists(os.path.join(url, '.svn/entries')):
281 281 self.wc = url
282 282 else:
283 283 self.wc = None
284 284 self.convertfp = None
285 285
286 286 def setrevmap(self, revmap):
287 287 lastrevs = {}
288 288 for revid in revmap.iterkeys():
289 289 uuid, module, revnum = self.revsplit(revid)
290 290 lastrevnum = lastrevs.setdefault(module, revnum)
291 291 if revnum > lastrevnum:
292 292 lastrevs[module] = revnum
293 293 self.lastrevs = lastrevs
294 294
295 295 def exists(self, path, optrev):
296 296 try:
297 297 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
298 298 optrev, False, self.ctx)
299 299 return True
300 300 except SubversionException:
301 301 return False
302 302
303 303 def getheads(self):
304 304
305 305 def isdir(path, revnum):
306 306 kind = self._checkpath(path, revnum)
307 307 return kind == svn.core.svn_node_dir
308 308
309 309 def getcfgpath(name, rev):
310 310 cfgpath = self.ui.config('convert', 'svn.' + name)
311 311 if cfgpath is not None and cfgpath.strip() == '':
312 312 return None
313 313 path = (cfgpath or name).strip('/')
314 314 if not self.exists(path, rev):
315 315 if cfgpath:
316 316 raise util.Abort(_('expected %s to be at %r, but not found')
317 317 % (name, path))
318 318 return None
319 319 self.ui.note(_('found %s at %r\n') % (name, path))
320 320 return path
321 321
322 322 rev = optrev(self.last_changed)
323 323 oldmodule = ''
324 324 trunk = getcfgpath('trunk', rev)
325 325 self.tags = getcfgpath('tags', rev)
326 326 branches = getcfgpath('branches', rev)
327 327
328 328 # If the project has a trunk or branches, we will extract heads
329 329 # from them. We keep the project root otherwise.
330 330 if trunk:
331 331 oldmodule = self.module or ''
332 332 self.module += '/' + trunk
333 333 self.head = self.latest(self.module, self.last_changed)
334 334 if not self.head:
335 335 raise util.Abort(_('no revision found in module %s')
336 336 % self.module)
337 337
338 338 # First head in the list is the module's head
339 339 self.heads = [self.head]
340 340 if self.tags is not None:
341 341 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
342 342
343 343 # Check if branches bring a few more heads to the list
344 344 if branches:
345 345 rpath = self.url.strip('/')
346 346 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
347 347 rev, False, self.ctx)
348 348 for branch in branchnames.keys():
349 349 module = '%s/%s/%s' % (oldmodule, branches, branch)
350 350 if not isdir(module, self.last_changed):
351 351 continue
352 352 brevid = self.latest(module, self.last_changed)
353 353 if not brevid:
354 354 self.ui.note(_('ignoring empty branch %s\n') % branch)
355 355 continue
356 356 self.ui.note(_('found branch %s at %d\n') %
357 357 (branch, self.revnum(brevid)))
358 358 self.heads.append(brevid)
359 359
360 360 if self.startrev and self.heads:
361 361 if len(self.heads) > 1:
362 362 raise util.Abort(_('svn: start revision is not supported '
363 363 'with more than one branch'))
364 364 revnum = self.revnum(self.heads[0])
365 365 if revnum < self.startrev:
366 366 raise util.Abort(
367 367 _('svn: no revision found after start revision %d')
368 368 % self.startrev)
369 369
370 370 return self.heads
371 371
372 372 def getfile(self, file, rev):
373 373 data, mode = self._getfile(file, rev)
374 374 self.modecache[(file, rev)] = mode
375 375 return data
376 376
377 377 def getmode(self, file, rev):
378 378 return self.modecache[(file, rev)]
379 379
380 380 def getchanges(self, rev):
381 381 if self._changescache and self._changescache[0] == rev:
382 382 return self._changescache[1]
383 383 self._changescache = None
384 384 self.modecache = {}
385 385 (paths, parents) = self.paths[rev]
386 386 if parents:
387 387 files, copies = self.expandpaths(rev, paths, parents)
388 388 else:
389 389 # Perform a full checkout on roots
390 390 uuid, module, revnum = self.revsplit(rev)
391 391 entries = svn.client.ls(self.baseurl + urllib.quote(module),
392 392 optrev(revnum), True, self.ctx)
393 393 files = [n for n, e in entries.iteritems()
394 394 if e.kind == svn.core.svn_node_file]
395 395 copies = {}
396 396
397 397 files.sort()
398 398 files = zip(files, [rev] * len(files))
399 399
400 400 # caller caches the result, so free it here to release memory
401 401 del self.paths[rev]
402 402 return (files, copies)
403 403
404 404 def getchangedfiles(self, rev, i):
405 405 changes = self.getchanges(rev)
406 406 self._changescache = (rev, changes)
407 407 return [f[0] for f in changes[0]]
408 408
409 409 def getcommit(self, rev):
410 410 if rev not in self.commits:
411 411 uuid, module, revnum = self.revsplit(rev)
412 412 self.module = module
413 413 self.reparent(module)
414 414 # We assume that:
415 415 # - requests for revisions after "stop" come from the
416 416 # revision graph backward traversal. Cache all of them
417 417 # down to stop, they will be used eventually.
418 418 # - requests for revisions before "stop" come to get
419 419 # isolated branches parents. Just fetch what is needed.
420 420 stop = self.lastrevs.get(module, 0)
421 421 if revnum < stop:
422 422 stop = revnum + 1
423 423 self._fetch_revisions(revnum, stop)
424 424 commit = self.commits[rev]
425 425 # caller caches the result, so free it here to release memory
426 426 del self.commits[rev]
427 427 return commit
428 428
429 429 def gettags(self):
430 430 tags = {}
431 431 if self.tags is None:
432 432 return tags
433 433
434 434 # svn tags are just a convention, project branches left in a
435 435 # 'tags' directory. There is no other relationship than
436 436 # ancestry, which is expensive to discover and makes them hard
437 437 # to update incrementally. Worse, past revisions may be
438 438 # referenced by tags far away in the future, requiring a deep
439 439 # history traversal on every calculation. Current code
440 440 # performs a single backward traversal, tracking moves within
441 441 # the tags directory (tag renaming) and recording a new tag
442 442 # everytime a project is copied from outside the tags
443 443 # directory. It also lists deleted tags, this behaviour may
444 444 # change in the future.
445 445 pendings = []
446 446 tagspath = self.tags
447 447 start = svn.ra.get_latest_revnum(self.ra)
448 448 try:
449 449 for entry in self._getlog([self.tags], start, self.startrev):
450 450 origpaths, revnum, author, date, message = entry
451 451 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
452 452 in origpaths.iteritems() if e.copyfrom_path]
453 453 # Apply moves/copies from more specific to general
454 454 copies.sort(reverse=True)
455 455
456 456 srctagspath = tagspath
457 457 if copies and copies[-1][2] == tagspath:
458 458 # Track tags directory moves
459 459 srctagspath = copies.pop()[0]
460 460
461 461 for source, sourcerev, dest in copies:
462 462 if not dest.startswith(tagspath + '/'):
463 463 continue
464 464 for tag in pendings:
465 465 if tag[0].startswith(dest):
466 466 tagpath = source + tag[0][len(dest):]
467 467 tag[:2] = [tagpath, sourcerev]
468 468 break
469 469 else:
470 470 pendings.append([source, sourcerev, dest])
471 471
472 472 # Filter out tags with children coming from different
473 473 # parts of the repository like:
474 474 # /tags/tag.1 (from /trunk:10)
475 475 # /tags/tag.1/foo (from /branches/foo:12)
476 476 # Here/tags/tag.1 discarded as well as its children.
477 477 # It happens with tools like cvs2svn. Such tags cannot
478 478 # be represented in mercurial.
479 479 addeds = dict((p, e.copyfrom_path) for p, e
480 480 in origpaths.iteritems()
481 481 if e.action == 'A' and e.copyfrom_path)
482 482 badroots = set()
483 483 for destroot in addeds:
484 484 for source, sourcerev, dest in pendings:
485 485 if (not dest.startswith(destroot + '/')
486 486 or source.startswith(addeds[destroot] + '/')):
487 487 continue
488 488 badroots.add(destroot)
489 489 break
490 490
491 491 for badroot in badroots:
492 492 pendings = [p for p in pendings if p[2] != badroot
493 493 and not p[2].startswith(badroot + '/')]
494 494
495 495 # Tell tag renamings from tag creations
496 496 remainings = []
497 497 for source, sourcerev, dest in pendings:
498 498 tagname = dest.split('/')[-1]
499 499 if source.startswith(srctagspath):
500 500 remainings.append([source, sourcerev, tagname])
501 501 continue
502 502 if tagname in tags:
503 503 # Keep the latest tag value
504 504 continue
505 505 # From revision may be fake, get one with changes
506 506 try:
507 507 tagid = self.latest(source, sourcerev)
508 508 if tagid and tagname not in tags:
509 509 tags[tagname] = tagid
510 510 except SvnPathNotFound:
511 511 # It happens when we are following directories
512 512 # we assumed were copied with their parents
513 513 # but were really created in the tag
514 514 # directory.
515 515 pass
516 516 pendings = remainings
517 517 tagspath = srctagspath
518 518
519 519 except SubversionException:
520 520 self.ui.note(_('no tags found at revision %d\n') % start)
521 521 return tags
522 522
523 523 def converted(self, rev, destrev):
524 524 if not self.wc:
525 525 return
526 526 if self.convertfp is None:
527 527 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
528 528 'a')
529 529 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
530 530 self.convertfp.flush()
531 531
532 532 def revid(self, revnum, module=None):
533 533 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
534 534
535 535 def revnum(self, rev):
536 536 return int(rev.split('@')[-1])
537 537
538 538 def revsplit(self, rev):
539 539 url, revnum = rev.rsplit('@', 1)
540 540 revnum = int(revnum)
541 541 parts = url.split('/', 1)
542 542 uuid = parts.pop(0)[4:]
543 543 mod = ''
544 544 if parts:
545 545 mod = '/' + parts[0]
546 546 return uuid, mod, revnum
547 547
548 548 def latest(self, path, stop=0):
549 549 """Find the latest revid affecting path, up to stop. It may return
550 550 a revision in a different module, since a branch may be moved without
551 551 a change being reported. Return None if computed module does not
552 552 belong to rootmodule subtree.
553 553 """
554 554 if not path.startswith(self.rootmodule):
555 555 # Requests on foreign branches may be forbidden at server level
556 556 self.ui.debug('ignoring foreign branch %r\n' % path)
557 557 return None
558 558
559 559 if not stop:
560 560 stop = svn.ra.get_latest_revnum(self.ra)
561 561 try:
562 562 prevmodule = self.reparent('')
563 563 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
564 564 self.reparent(prevmodule)
565 565 except SubversionException:
566 566 dirent = None
567 567 if not dirent:
568 568 raise SvnPathNotFound(_('%s not found up to revision %d')
569 569 % (path, stop))
570 570
571 571 # stat() gives us the previous revision on this line of
572 572 # development, but it might be in *another module*. Fetch the
573 573 # log and detect renames down to the latest revision.
574 574 stream = self._getlog([path], stop, dirent.created_rev)
575 575 try:
576 576 for entry in stream:
577 577 paths, revnum, author, date, message = entry
578 578 if revnum <= dirent.created_rev:
579 579 break
580 580
581 581 for p in paths:
582 582 if not path.startswith(p) or not paths[p].copyfrom_path:
583 583 continue
584 584 newpath = paths[p].copyfrom_path + path[len(p):]
585 585 self.ui.debug("branch renamed from %s to %s at %d\n" %
586 586 (path, newpath, revnum))
587 587 path = newpath
588 588 break
589 589 finally:
590 590 stream.close()
591 591
592 592 if not path.startswith(self.rootmodule):
593 593 self.ui.debug('ignoring foreign branch %r\n' % path)
594 594 return None
595 595 return self.revid(dirent.created_rev, path)
596 596
597 597 def reparent(self, module):
598 598 """Reparent the svn transport and return the previous parent."""
599 599 if self.prevmodule == module:
600 600 return module
601 601 svnurl = self.baseurl + urllib.quote(module)
602 602 prevmodule = self.prevmodule
603 603 if prevmodule is None:
604 604 prevmodule = ''
605 605 self.ui.debug("reparent to %s\n" % svnurl)
606 606 svn.ra.reparent(self.ra, svnurl)
607 607 self.prevmodule = module
608 608 return prevmodule
609 609
610 610 def expandpaths(self, rev, paths, parents):
611 611 entries = []
612 612 # Map of entrypath, revision for finding source of deleted
613 613 # revisions.
614 614 copyfrom = {}
615 615 copies = {}
616 616
617 617 new_module, revnum = self.revsplit(rev)[1:]
618 618 if new_module != self.module:
619 619 self.module = new_module
620 620 self.reparent(self.module)
621 621
622 622 for path, ent in paths:
623 623 entrypath = self.getrelpath(path)
624 624
625 625 kind = self._checkpath(entrypath, revnum)
626 626 if kind == svn.core.svn_node_file:
627 627 entries.append(self.recode(entrypath))
628 628 if not ent.copyfrom_path or not parents:
629 629 continue
630 630 # Copy sources not in parent revisions cannot be
631 631 # represented, ignore their origin for now
632 632 pmodule, prevnum = self.revsplit(parents[0])[1:]
633 633 if ent.copyfrom_rev < prevnum:
634 634 continue
635 635 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
636 636 if not copyfrom_path:
637 637 continue
638 638 self.ui.debug("copied to %s from %s@%s\n" %
639 639 (entrypath, copyfrom_path, ent.copyfrom_rev))
640 640 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
641 641 elif kind == 0: # gone, but had better be a deleted *file*
642 642 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
643 643 pmodule, prevnum = self.revsplit(parents[0])[1:]
644 644 parentpath = pmodule + "/" + entrypath
645 645 self.ui.debug("entry %s\n" % parentpath)
646 646
647 647 # We can avoid the reparent calls if the module has
648 648 # not changed but it probably does not worth the pain.
649 649 prevmodule = self.reparent('')
650 650 fromkind = svn.ra.check_path(self.ra, parentpath.strip('/'),
651 651 prevnum)
652 652 self.reparent(prevmodule)
653 653
654 654 if fromkind == svn.core.svn_node_file:
655 655 entries.append(self.recode(entrypath))
656 656 elif fromkind == svn.core.svn_node_dir:
657 657 if ent.action == 'C':
658 658 children = self._find_children(path, prevnum)
659 659 else:
660 660 oroot = parentpath.strip('/')
661 661 nroot = path.strip('/')
662 662 children = self._find_children(oroot, prevnum)
663 663 children = [s.replace(oroot, nroot) for s in children]
664 664
665 665 for child in children:
666 666 childpath = self.getrelpath("/" + child, pmodule)
667 667 if not childpath:
668 668 continue
669 669 if childpath in copies:
670 670 del copies[childpath]
671 671 entries.append(childpath)
672 672 else:
673 673 self.ui.debug('unknown path in revision %d: %s\n' % \
674 674 (revnum, path))
675 675 elif kind == svn.core.svn_node_dir:
676 676 # If the directory just had a prop change,
677 677 # then we shouldn't need to look for its children.
678 678 if ent.action == 'M':
679 679 continue
680 680
681 681 children = sorted(self._find_children(path, revnum))
682 682 for child in children:
683 683 # Can we move a child directory and its
684 684 # parent in the same commit? (probably can). Could
685 685 # cause problems if instead of revnum -1,
686 686 # we have to look in (copyfrom_path, revnum - 1)
687 687 entrypath = self.getrelpath("/" + child)
688 688 if entrypath:
689 689 # Need to filter out directories here...
690 690 kind = self._checkpath(entrypath, revnum)
691 691 if kind != svn.core.svn_node_dir:
692 692 entries.append(self.recode(entrypath))
693 693
694 694 # Handle directory copies
695 695 if not ent.copyfrom_path or not parents:
696 696 continue
697 697 # Copy sources not in parent revisions cannot be
698 698 # represented, ignore their origin for now
699 699 pmodule, prevnum = self.revsplit(parents[0])[1:]
700 700 if ent.copyfrom_rev < prevnum:
701 701 continue
702 702 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
703 703 if not copyfrompath:
704 704 continue
705 705 copyfrom[path] = ent
706 706 self.ui.debug("mark %s came from %s:%d\n"
707 707 % (path, copyfrompath, ent.copyfrom_rev))
708 708 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
709 709 children.sort()
710 710 for child in children:
711 711 entrypath = self.getrelpath("/" + child, pmodule)
712 712 if not entrypath:
713 713 continue
714 714 copytopath = path + entrypath[len(copyfrompath):]
715 715 copytopath = self.getrelpath(copytopath)
716 716 copies[self.recode(copytopath)] = self.recode(entrypath)
717 717
718 718 return (list(set(entries)), copies)
719 719
720 720 def _fetch_revisions(self, from_revnum, to_revnum):
721 721 if from_revnum < to_revnum:
722 722 from_revnum, to_revnum = to_revnum, from_revnum
723 723
724 724 self.child_cset = None
725 725
726 726 def parselogentry(orig_paths, revnum, author, date, message):
727 727 """Return the parsed commit object or None, and True if
728 728 the revision is a branch root.
729 729 """
730 730 self.ui.debug("parsing revision %d (%d changes)\n" %
731 731 (revnum, len(orig_paths)))
732 732
733 733 branched = False
734 734 rev = self.revid(revnum)
735 735 # branch log might return entries for a parent we already have
736 736
737 737 if rev in self.commits or revnum < to_revnum:
738 738 return None, branched
739 739
740 740 parents = []
741 741 # check whether this revision is the start of a branch or part
742 742 # of a branch renaming
743 743 orig_paths = sorted(orig_paths.iteritems())
744 744 root_paths = [(p, e) for p, e in orig_paths
745 745 if self.module.startswith(p)]
746 746 if root_paths:
747 747 path, ent = root_paths[-1]
748 748 if ent.copyfrom_path:
749 749 branched = True
750 750 newpath = ent.copyfrom_path + self.module[len(path):]
751 751 # ent.copyfrom_rev may not be the actual last revision
752 752 previd = self.latest(newpath, ent.copyfrom_rev)
753 753 if previd is not None:
754 754 prevmodule, prevnum = self.revsplit(previd)[1:]
755 755 if prevnum >= self.startrev:
756 756 parents = [previd]
757 757 self.ui.note(
758 758 _('found parent of branch %s at %d: %s\n') %
759 759 (self.module, prevnum, prevmodule))
760 760 else:
761 761 self.ui.debug("no copyfrom path, don't know what to do.\n")
762 762
763 763 paths = []
764 764 # filter out unrelated paths
765 765 for path, ent in orig_paths:
766 766 if self.getrelpath(path) is None:
767 767 continue
768 768 paths.append((path, ent))
769 769
770 770 # Example SVN datetime. Includes microseconds.
771 771 # ISO-8601 conformant
772 772 # '2007-01-04T17:35:00.902377Z'
773 773 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
774 774
775 775 log = message and self.recode(message) or ''
776 776 author = author and self.recode(author) or ''
777 777 try:
778 778 branch = self.module.split("/")[-1]
779 779 if branch == 'trunk':
780 780 branch = ''
781 781 except IndexError:
782 782 branch = None
783 783
784 784 cset = commit(author=author,
785 785 date=util.datestr(date),
786 786 desc=log,
787 787 parents=parents,
788 788 branch=branch,
789 789 rev=rev)
790 790
791 791 self.commits[rev] = cset
792 792 # The parents list is *shared* among self.paths and the
793 793 # commit object. Both will be updated below.
794 794 self.paths[rev] = (paths, cset.parents)
795 795 if self.child_cset and not self.child_cset.parents:
796 796 self.child_cset.parents[:] = [rev]
797 797 self.child_cset = cset
798 798 return cset, branched
799 799
800 800 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
801 801 (self.module, from_revnum, to_revnum))
802 802
803 803 try:
804 804 firstcset = None
805 805 lastonbranch = False
806 806 stream = self._getlog([self.module], from_revnum, to_revnum)
807 807 try:
808 808 for entry in stream:
809 809 paths, revnum, author, date, message = entry
810 810 if revnum < self.startrev:
811 811 lastonbranch = True
812 812 break
813 813 if not paths:
814 814 self.ui.debug('revision %d has no entries\n' % revnum)
815 815 # If we ever leave the loop on an empty
816 816 # revision, do not try to get a parent branch
817 817 lastonbranch = lastonbranch or revnum == 0
818 818 continue
819 819 cset, lastonbranch = parselogentry(paths, revnum, author,
820 820 date, message)
821 821 if cset:
822 822 firstcset = cset
823 823 if lastonbranch:
824 824 break
825 825 finally:
826 826 stream.close()
827 827
828 828 if not lastonbranch and firstcset and not firstcset.parents:
829 829 # The first revision of the sequence (the last fetched one)
830 830 # has invalid parents if not a branch root. Find the parent
831 831 # revision now, if any.
832 832 try:
833 833 firstrevnum = self.revnum(firstcset.rev)
834 834 if firstrevnum > 1:
835 835 latest = self.latest(self.module, firstrevnum - 1)
836 836 if latest:
837 837 firstcset.parents.append(latest)
838 838 except SvnPathNotFound:
839 839 pass
840 840 except SubversionException, (inst, num):
841 841 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
842 842 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
843 843 raise
844 844
845 845 def _getfile(self, file, rev):
846 846 # TODO: ra.get_file transmits the whole file instead of diffs.
847 847 mode = ''
848 848 try:
849 849 new_module, revnum = self.revsplit(rev)[1:]
850 850 if self.module != new_module:
851 851 self.module = new_module
852 852 self.reparent(self.module)
853 853 io = StringIO()
854 854 info = svn.ra.get_file(self.ra, file, revnum, io)
855 855 data = io.getvalue()
856 856 # ra.get_files() seems to keep a reference on the input buffer
857 857 # preventing collection. Release it explicitely.
858 858 io.close()
859 859 if isinstance(info, list):
860 860 info = info[-1]
861 861 mode = ("svn:executable" in info) and 'x' or ''
862 862 mode = ("svn:special" in info) and 'l' or mode
863 863 except SubversionException, e:
864 864 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
865 865 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
866 866 if e.apr_err in notfound: # File not found
867 867 raise IOError()
868 868 raise
869 869 if mode == 'l':
870 870 link_prefix = "link "
871 871 if data.startswith(link_prefix):
872 872 data = data[len(link_prefix):]
873 873 return data, mode
874 874
875 875 def _find_children(self, path, revnum):
876 876 path = path.strip('/')
877 877 pool = Pool()
878 878 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
879 879 return ['%s/%s' % (path, x) for x in
880 880 svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
881 881
882 882 def getrelpath(self, path, module=None):
883 883 if module is None:
884 884 module = self.module
885 885 # Given the repository url of this wc, say
886 886 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
887 887 # extract the "entry" portion (a relative path) from what
888 888 # svn log --xml says, ie
889 889 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
890 890 # that is to say "tests/PloneTestCase.py"
891 891 if path.startswith(module):
892 892 relative = path.rstrip('/')[len(module):]
893 893 if relative.startswith('/'):
894 894 return relative[1:]
895 895 elif relative == '':
896 896 return relative
897 897
898 898 # The path is outside our tracked tree...
899 899 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
900 900 return None
901 901
902 902 def _checkpath(self, path, revnum):
903 903 # ra.check_path does not like leading slashes very much, it leads
904 904 # to PROPFIND subversion errors
905 905 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
906 906
907 907 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
908 908 strict_node_history=False):
909 909 # Normalize path names, svn >= 1.5 only wants paths relative to
910 910 # supplied URL
911 911 relpaths = []
912 912 for p in paths:
913 913 if not p.startswith('/'):
914 914 p = self.module + '/' + p
915 915 relpaths.append(p.strip('/'))
916 916 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
917 917 strict_node_history]
918 918 arg = encodeargs(args)
919 919 hgexe = util.hgexecutable()
920 920 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
921 921 stdin, stdout = util.popen2(cmd)
922 922 stdin.write(arg)
923 923 try:
924 924 stdin.close()
925 925 except IOError:
926 926 raise util.Abort(_('Mercurial failed to run itself, check'
927 927 ' hg executable is in PATH'))
928 928 return logstream(stdout)
929 929
930 930 pre_revprop_change = '''#!/bin/sh
931 931
932 932 REPOS="$1"
933 933 REV="$2"
934 934 USER="$3"
935 935 PROPNAME="$4"
936 936 ACTION="$5"
937 937
938 938 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
939 939 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
940 940 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
941 941
942 942 echo "Changing prohibited revision property" >&2
943 943 exit 1
944 944 '''
945 945
946 946 class svn_sink(converter_sink, commandline):
947 947 commit_re = re.compile(r'Committed revision (\d+).', re.M)
948 948
949 949 def prerun(self):
950 950 if self.wc:
951 951 os.chdir(self.wc)
952 952
953 953 def postrun(self):
954 954 if self.wc:
955 955 os.chdir(self.cwd)
956 956
957 957 def join(self, name):
958 958 return os.path.join(self.wc, '.svn', name)
959 959
960 960 def revmapfile(self):
961 961 return self.join('hg-shamap')
962 962
963 963 def authorfile(self):
964 964 return self.join('hg-authormap')
965 965
966 966 def __init__(self, ui, path):
967 967 converter_sink.__init__(self, ui, path)
968 968 commandline.__init__(self, ui, 'svn')
969 969 self.delete = []
970 970 self.setexec = []
971 971 self.delexec = []
972 972 self.copies = []
973 973 self.wc = None
974 974 self.cwd = os.getcwd()
975 975
976 976 path = os.path.realpath(path)
977 977
978 978 created = False
979 979 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
980 980 self.wc = path
981 981 self.run0('update')
982 982 else:
983 983 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
984 984
985 985 if os.path.isdir(os.path.dirname(path)):
986 986 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
987 987 ui.status(_('initializing svn repo %r\n') %
988 988 os.path.basename(path))
989 989 commandline(ui, 'svnadmin').run0('create', path)
990 990 created = path
991 991 path = util.normpath(path)
992 992 if not path.startswith('/'):
993 993 path = '/' + path
994 994 path = 'file://' + path
995 995
996 996 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
997 997 self.run0('checkout', path, wcpath)
998 998
999 999 self.wc = wcpath
1000 1000 self.opener = util.opener(self.wc)
1001 1001 self.wopener = util.opener(self.wc)
1002 1002 self.childmap = mapfile(ui, self.join('hg-childmap'))
1003 1003 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1004 1004
1005 1005 if created:
1006 1006 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1007 1007 fp = open(hook, 'w')
1008 1008 fp.write(pre_revprop_change)
1009 1009 fp.close()
1010 1010 util.set_flags(hook, False, True)
1011 1011
1012 1012 xport = transport.SvnRaTransport(url=geturl(path))
1013 1013 self.uuid = svn.ra.get_uuid(xport.ra)
1014 1014
1015 1015 def wjoin(self, *names):
1016 1016 return os.path.join(self.wc, *names)
1017 1017
1018 1018 def putfile(self, filename, flags, data):
1019 1019 if 'l' in flags:
1020 1020 self.wopener.symlink(data, filename)
1021 1021 else:
1022 1022 try:
1023 1023 if os.path.islink(self.wjoin(filename)):
1024 1024 os.unlink(filename)
1025 1025 except OSError:
1026 1026 pass
1027 1027 self.wopener(filename, 'w').write(data)
1028 1028
1029 1029 if self.is_exec:
1030 1030 was_exec = self.is_exec(self.wjoin(filename))
1031 1031 else:
1032 1032 # On filesystems not supporting execute-bit, there is no way
1033 1033 # to know if it is set but asking subversion. Setting it
1034 1034 # systematically is just as expensive and much simpler.
1035 1035 was_exec = 'x' not in flags
1036 1036
1037 1037 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1038 1038 if was_exec:
1039 1039 if 'x' not in flags:
1040 1040 self.delexec.append(filename)
1041 1041 else:
1042 1042 if 'x' in flags:
1043 1043 self.setexec.append(filename)
1044 1044
1045 1045 def _copyfile(self, source, dest):
1046 1046 # SVN's copy command pukes if the destination file exists, but
1047 1047 # our copyfile method expects to record a copy that has
1048 1048 # already occurred. Cross the semantic gap.
1049 1049 wdest = self.wjoin(dest)
1050 1050 exists = os.path.exists(wdest)
1051 1051 if exists:
1052 1052 fd, tempname = tempfile.mkstemp(
1053 1053 prefix='hg-copy-', dir=os.path.dirname(wdest))
1054 1054 os.close(fd)
1055 1055 os.unlink(tempname)
1056 1056 os.rename(wdest, tempname)
1057 1057 try:
1058 1058 self.run0('copy', source, dest)
1059 1059 finally:
1060 1060 if exists:
1061 1061 try:
1062 1062 os.unlink(wdest)
1063 1063 except OSError:
1064 1064 pass
1065 1065 os.rename(tempname, wdest)
1066 1066
1067 1067 def dirs_of(self, files):
1068 1068 dirs = set()
1069 1069 for f in files:
1070 1070 if os.path.isdir(self.wjoin(f)):
1071 1071 dirs.add(f)
1072 1072 for i in strutil.rfindall(f, '/'):
1073 1073 dirs.add(f[:i])
1074 1074 return dirs
1075 1075
1076 1076 def add_dirs(self, files):
1077 1077 add_dirs = [d for d in sorted(self.dirs_of(files))
1078 1078 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1079 1079 if add_dirs:
1080 1080 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1081 1081 return add_dirs
1082 1082
1083 1083 def add_files(self, files):
1084 1084 if files:
1085 1085 self.xargs(files, 'add', quiet=True)
1086 1086 return files
1087 1087
1088 1088 def tidy_dirs(self, names):
1089 1089 deleted = []
1090 1090 for d in sorted(self.dirs_of(names), reverse=True):
1091 1091 wd = self.wjoin(d)
1092 1092 if os.listdir(wd) == '.svn':
1093 1093 self.run0('delete', d)
1094 1094 deleted.append(d)
1095 1095 return deleted
1096 1096
1097 1097 def addchild(self, parent, child):
1098 1098 self.childmap[parent] = child
1099 1099
1100 1100 def revid(self, rev):
1101 1101 return u"svn:%s@%s" % (self.uuid, rev)
1102 1102
1103 1103 def putcommit(self, files, copies, parents, commit, source, revmap):
1104 1104 # Apply changes to working copy
1105 1105 for f, v in files:
1106 1106 try:
1107 1107 data = source.getfile(f, v)
1108 1108 except IOError:
1109 1109 self.delete.append(f)
1110 1110 else:
1111 1111 e = source.getmode(f, v)
1112 1112 self.putfile(f, e, data)
1113 1113 if f in copies:
1114 1114 self.copies.append([copies[f], f])
1115 1115 files = [f[0] for f in files]
1116 1116
1117 1117 for parent in parents:
1118 1118 try:
1119 1119 return self.revid(self.childmap[parent])
1120 1120 except KeyError:
1121 1121 pass
1122 1122 entries = set(self.delete)
1123 1123 files = frozenset(files)
1124 1124 entries.update(self.add_dirs(files.difference(entries)))
1125 1125 if self.copies:
1126 1126 for s, d in self.copies:
1127 1127 self._copyfile(s, d)
1128 1128 self.copies = []
1129 1129 if self.delete:
1130 1130 self.xargs(self.delete, 'delete')
1131 1131 self.delete = []
1132 1132 entries.update(self.add_files(files.difference(entries)))
1133 1133 entries.update(self.tidy_dirs(entries))
1134 1134 if self.delexec:
1135 1135 self.xargs(self.delexec, 'propdel', 'svn:executable')
1136 1136 self.delexec = []
1137 1137 if self.setexec:
1138 1138 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1139 1139 self.setexec = []
1140 1140
1141 1141 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1142 1142 fp = os.fdopen(fd, 'w')
1143 1143 fp.write(commit.desc)
1144 1144 fp.close()
1145 1145 try:
1146 1146 output = self.run0('commit',
1147 1147 username=util.shortuser(commit.author),
1148 1148 file=messagefile,
1149 1149 encoding='utf-8')
1150 1150 try:
1151 1151 rev = self.commit_re.search(output).group(1)
1152 1152 except AttributeError:
1153 1153 if not files:
1154 1154 return parents[0]
1155 1155 self.ui.warn(_('unexpected svn output:\n'))
1156 1156 self.ui.warn(output)
1157 1157 raise util.Abort(_('unable to cope with svn output'))
1158 1158 if commit.rev:
1159 1159 self.run('propset', 'hg:convert-rev', commit.rev,
1160 1160 revprop=True, revision=rev)
1161 1161 if commit.branch and commit.branch != 'default':
1162 1162 self.run('propset', 'hg:convert-branch', commit.branch,
1163 1163 revprop=True, revision=rev)
1164 1164 for parent in parents:
1165 1165 self.addchild(parent, rev)
1166 1166 return self.revid(rev)
1167 1167 finally:
1168 1168 os.unlink(messagefile)
1169 1169
1170 1170 def puttags(self, tags):
1171 1171 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
@@ -1,65 +1,68 b''
1 1 #!/bin/sh
2 2
3 3 cat >> $HGRCPATH <<EOF
4 4 [extensions]
5 5 convert=
6 6 [convert]
7 7 hg.saverev=False
8 8 EOF
9 9
10 10 hg help convert
11 11
12 12 hg init a
13 13 cd a
14 14 echo a > a
15 15 hg ci -d'0 0' -Ama
16 16 hg cp a b
17 17 hg ci -d'1 0' -mb
18 18 hg rm a
19 19 hg ci -d'2 0' -mc
20 20 hg mv b a
21 21 hg ci -d'3 0' -md
22 22 echo a >> a
23 23 hg ci -d'4 0' -me
24 24
25 25 cd ..
26 26 hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
27 27 hg --cwd a-hg pull ../a
28 28
29 29 touch bogusfile
30 30 echo % should fail
31 31 hg convert a bogusfile
32 32
33 33 mkdir bogusdir
34 34 chmod 000 bogusdir
35 35
36 36 echo % should fail
37 37 hg convert a bogusdir
38 38
39 39 echo % should succeed
40 40 chmod 700 bogusdir
41 41 hg convert a bogusdir
42 42
43 43 echo % test pre and post conversion actions
44 44 echo 'include b' > filemap
45 45 hg convert --debug --filemap filemap a partialb | \
46 46 grep 'run hg'
47 47
48 48 echo % converting empty dir should fail "nicely"
49 49 mkdir emptydir
50 50 # override $PATH to ensure p4 not visible; use $PYTHON in case we're
51 51 # running from a devel copy, not a temp installation
52 52 PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir 2>&1 | sed 's,file://.*/emptydir,.../emptydir,g'
53 53
54 54 echo % convert with imaginary source type
55 55 hg convert --source-type foo a a-foo
56 56 echo % convert with imaginary sink type
57 57 hg convert --dest-type foo a a-foo
58 58
59 59 echo
60 60 echo % "testing: convert must not produce duplicate entries in fncache"
61 61 hg convert a b
62 62 echo % "contents of fncache file:"
63 63 cat b/.hg/store/fncache
64 64
65 echo '% test bogus URL'
66 hg convert -q bzr+ssh://foobar@selenic.com/baz baz
67
65 68 true
@@ -1,289 +1,291 b''
1 1 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
2 2
3 3 convert a foreign SCM repository to a Mercurial one.
4 4
5 5 Accepted source formats [identifiers]:
6 6
7 7 - Mercurial [hg]
8 8 - CVS [cvs]
9 9 - Darcs [darcs]
10 10 - git [git]
11 11 - Subversion [svn]
12 12 - Monotone [mtn]
13 13 - GNU Arch [gnuarch]
14 14 - Bazaar [bzr]
15 15 - Perforce [p4]
16 16
17 17 Accepted destination formats [identifiers]:
18 18
19 19 - Mercurial [hg]
20 20 - Subversion [svn] (history on branches is not preserved)
21 21
22 22 If no revision is given, all revisions will be converted. Otherwise,
23 23 convert will only import up to the named revision (given in a format
24 24 understood by the source).
25 25
26 26 If no destination directory name is specified, it defaults to the basename
27 27 of the source with '-hg' appended. If the destination repository doesn't
28 28 exist, it will be created.
29 29
30 30 By default, all sources except Mercurial will use --branchsort. Mercurial
31 31 uses --sourcesort to preserve original revision numbers order. Sort modes
32 32 have the following effects:
33 33
34 34 --branchsort convert from parent to child revision when possible, which
35 35 means branches are usually converted one after the other. It
36 36 generates more compact repositories.
37 37 --datesort sort revisions by date. Converted repositories have good-
38 38 looking changelogs but are often an order of magnitude
39 39 larger than the same ones generated by --branchsort.
40 40 --sourcesort try to preserve source revisions order, only supported by
41 41 Mercurial sources.
42 42
43 43 If <REVMAP> isn't given, it will be put in a default location
44 44 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text file that
45 45 maps each source commit ID to the destination ID for that revision, like
46 46 so:
47 47
48 48 <source ID> <destination ID>
49 49
50 50 If the file doesn't exist, it's automatically created. It's updated on
51 51 each commit copied, so convert-repo can be interrupted and can be run
52 52 repeatedly to copy new commits.
53 53
54 54 The [username mapping] file is a simple text file that maps each source
55 55 commit author to a destination commit author. It is handy for source SCMs
56 56 that use unix logins to identify authors (eg: CVS). One line per author
57 57 mapping and the line format is: srcauthor=whatever string you want
58 58
59 59 The filemap is a file that allows filtering and remapping of files and
60 60 directories. Comment lines start with '#'. Each line can contain one of
61 61 the following directives:
62 62
63 63 include path/to/file
64 64
65 65 exclude path/to/file
66 66
67 67 rename from/file to/file
68 68
69 69 The 'include' directive causes a file, or all files under a directory, to
70 70 be included in the destination repository, and the exclusion of all other
71 71 files and directories not explicitly included. The 'exclude' directive
72 72 causes files or directories to be omitted. The 'rename' directive renames
73 73 a file or directory. To rename from a subdirectory into the root of the
74 74 repository, use '.' as the path to rename to.
75 75
76 76 The splicemap is a file that allows insertion of synthetic history,
77 77 letting you specify the parents of a revision. This is useful if you want
78 78 to e.g. give a Subversion merge two parents, or graft two disconnected
79 79 series of history together. Each entry contains a key, followed by a
80 80 space, followed by one or two comma-separated values. The key is the
81 81 revision ID in the source revision control system whose parents should be
82 82 modified (same format as a key in .hg/shamap). The values are the revision
83 83 IDs (in either the source or destination revision control system) that
84 84 should be used as the new parents for that node. For example, if you have
85 85 merged "release-1.0" into "trunk", then you should specify the revision on
86 86 "trunk" as the first parent and the one on the "release-1.0" branch as the
87 87 second.
88 88
89 89 The branchmap is a file that allows you to rename a branch when it is
90 90 being brought in from whatever external repository. When used in
91 91 conjunction with a splicemap, it allows for a powerful combination to help
92 92 fix even the most badly mismanaged repositories and turn them into nicely
93 93 structured Mercurial repositories. The branchmap contains lines of the
94 94 form "original_branch_name new_branch_name". "original_branch_name" is the
95 95 name of the branch in the source repository, and "new_branch_name" is the
96 96 name of the branch is the destination repository. This can be used to (for
97 97 instance) move code in one repository from "default" to a named branch.
98 98
99 99 Mercurial Source
100 100 ----------------
101 101
102 102 --config convert.hg.ignoreerrors=False (boolean)
103 103 ignore integrity errors when reading. Use it to fix Mercurial
104 104 repositories with missing revlogs, by converting from and to
105 105 Mercurial.
106 106 --config convert.hg.saverev=False (boolean)
107 107 store original revision ID in changeset (forces target IDs to change)
108 108 --config convert.hg.startrev=0 (hg revision identifier)
109 109 convert start revision and its descendants
110 110
111 111 CVS Source
112 112 ----------
113 113
114 114 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
115 115 indicate the starting point of what will be converted. Direct access to
116 116 the repository files is not needed, unless of course the repository is
117 117 :local:. The conversion uses the top level directory in the sandbox to
118 118 find the CVS repository, and then uses CVS rlog commands to find files to
119 119 convert. This means that unless a filemap is given, all files under the
120 120 starting directory will be converted, and that any directory
121 121 reorganization in the CVS sandbox is ignored.
122 122
123 123 The options shown are the defaults.
124 124
125 125 --config convert.cvsps.cache=True (boolean)
126 126 Set to False to disable remote log caching, for testing and debugging
127 127 purposes.
128 128 --config convert.cvsps.fuzz=60 (integer)
129 129 Specify the maximum time (in seconds) that is allowed between commits
130 130 with identical user and log message in a single changeset. When very
131 131 large files were checked in as part of a changeset then the default
132 132 may not be long enough.
133 133 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
134 134 Specify a regular expression to which commit log messages are matched.
135 135 If a match occurs, then the conversion process will insert a dummy
136 136 revision merging the branch on which this log message occurs to the
137 137 branch indicated in the regex.
138 138 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
139 139 Specify a regular expression to which commit log messages are matched.
140 140 If a match occurs, then the conversion process will add the most
141 141 recent revision on the branch indicated in the regex as the second
142 142 parent of the changeset.
143 143 --config hook.cvslog
144 144 Specify a Python function to be called at the end of gathering the CVS
145 145 log. The function is passed a list with the log entries, and can
146 146 modify the entries in-place, or add or delete them.
147 147 --config hook.cvschangesets
148 148 Specify a Python function to be called after the changesets are
149 149 calculated from the the CVS log. The function is passed a list with
150 150 the changeset entries, and can modify the changesets in-place, or add
151 151 or delete them.
152 152
153 153 An additional "debugcvsps" Mercurial command allows the builtin changeset
154 154 merging code to be run without doing a conversion. Its parameters and
155 155 output are similar to that of cvsps 2.1. Please see the command help for
156 156 more details.
157 157
158 158 Subversion Source
159 159 -----------------
160 160
161 161 Subversion source detects classical trunk/branches/tags layouts. By
162 162 default, the supplied "svn://repo/path/" source URL is converted as a
163 163 single branch. If "svn://repo/path/trunk" exists it replaces the default
164 164 branch. If "svn://repo/path/branches" exists, its subdirectories are
165 165 listed as possible branches. If "svn://repo/path/tags" exists, it is
166 166 looked for tags referencing converted branches. Default "trunk",
167 167 "branches" and "tags" values can be overridden with following options. Set
168 168 them to paths relative to the source URL, or leave them blank to disable
169 169 auto detection.
170 170
171 171 --config convert.svn.branches=branches (directory name)
172 172 specify the directory containing branches
173 173 --config convert.svn.tags=tags (directory name)
174 174 specify the directory containing tags
175 175 --config convert.svn.trunk=trunk (directory name)
176 176 specify the name of the trunk branch
177 177
178 178 Source history can be retrieved starting at a specific revision, instead
179 179 of being integrally converted. Only single branch conversions are
180 180 supported.
181 181
182 182 --config convert.svn.startrev=0 (svn revision number)
183 183 specify start Subversion revision.
184 184
185 185 Perforce Source
186 186 ---------------
187 187
188 188 The Perforce (P4) importer can be given a p4 depot path or a client
189 189 specification as source. It will convert all files in the source to a flat
190 190 Mercurial repository, ignoring labels, branches and integrations. Note
191 191 that when a depot path is given you then usually should specify a target
192 192 directory, because otherwise the target may be named ...-hg.
193 193
194 194 It is possible to limit the amount of source history to be converted by
195 195 specifying an initial Perforce revision.
196 196
197 197 --config convert.p4.startrev=0 (perforce changelist number)
198 198 specify initial Perforce revision.
199 199
200 200 Mercurial Destination
201 201 ---------------------
202 202
203 203 --config convert.hg.clonebranches=False (boolean)
204 204 dispatch source branches in separate clones.
205 205 --config convert.hg.tagsbranch=default (branch name)
206 206 tag revisions branch name
207 207 --config convert.hg.usebranchnames=True (boolean)
208 208 preserve branch names
209 209
210 210 options:
211 211
212 212 -A --authors username mapping filename
213 213 -d --dest-type destination repository type
214 214 --filemap remap file names using contents of file
215 215 -r --rev import up to target revision REV
216 216 -s --source-type source repository type
217 217 --splicemap splice synthesized history into place
218 218 --branchmap change branch names while converting
219 219 --branchsort try to sort changesets by branches
220 220 --datesort try to sort changesets by date
221 221 --sourcesort preserve source changesets order
222 222
223 223 use "hg -v help convert" to show global options
224 224 adding a
225 225 assuming destination a-hg
226 226 initializing destination a-hg repository
227 227 scanning source...
228 228 sorting...
229 229 converting...
230 230 4 a
231 231 3 b
232 232 2 c
233 233 1 d
234 234 0 e
235 235 pulling from ../a
236 236 searching for changes
237 237 no changes found
238 238 % should fail
239 239 initializing destination bogusfile repository
240 240 abort: cannot create new bundle repository
241 241 % should fail
242 242 abort: Permission denied: bogusdir
243 243 % should succeed
244 244 initializing destination bogusdir repository
245 245 scanning source...
246 246 sorting...
247 247 converting...
248 248 4 a
249 249 3 b
250 250 2 c
251 251 1 d
252 252 0 e
253 253 % test pre and post conversion actions
254 254 run hg source pre-conversion action
255 255 run hg sink pre-conversion action
256 256 run hg sink post-conversion action
257 257 run hg source post-conversion action
258 258 % converting empty dir should fail nicely
259 259 assuming destination emptydir-hg
260 260 initializing destination emptydir-hg repository
261 261 emptydir does not look like a CVS checkout
262 262 emptydir does not look like a Git repo
263 263 emptydir does not look like a Subversion repo
264 264 emptydir is not a local Mercurial repo
265 265 emptydir does not look like a darcs repo
266 266 emptydir does not look like a monotone repo
267 267 emptydir does not look like a GNU Arch repo
268 268 emptydir does not look like a Bazaar repo
269 269 cannot find required "p4" tool
270 270 abort: emptydir: missing or unsupported repository
271 271 % convert with imaginary source type
272 272 initializing destination a-foo repository
273 273 abort: foo: invalid source repository type
274 274 % convert with imaginary sink type
275 275 abort: foo: invalid destination repository type
276 276
277 277 % testing: convert must not produce duplicate entries in fncache
278 278 initializing destination b repository
279 279 scanning source...
280 280 sorting...
281 281 converting...
282 282 4 a
283 283 3 b
284 284 2 c
285 285 1 d
286 286 0 e
287 287 % contents of fncache file:
288 288 data/a.i
289 289 data/b.i
290 % test bogus URL
291 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
General Comments 0
You need to be logged in to leave comments. Login now