##// END OF EJS Templates
convert: enable deterministic conversion progress bar for svn...
Augie Fackler -
r22414:299eaa09 default
parent child Browse files
Show More
@@ -1,1310 +1,1313 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, re, sys, tempfile, urllib, urllib2
6 6 import xml.dom.minidom
7 7 import cPickle as pickle
8 8
9 9 from mercurial import strutil, scmutil, util, encoding
10 10 from mercurial.i18n import _
11 11
12 12 propertycache = util.propertycache
13 13
14 14 # Subversion stuff. Works best with very recent Python SVN bindings
15 15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 16 # these bindings.
17 17
18 18 from cStringIO import StringIO
19 19
20 20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 21 from common import commandline, converter_source, converter_sink, mapfile
22 22 from common import makedatetimestamp
23 23
24 24 try:
25 25 from svn.core import SubversionException, Pool
26 26 import svn
27 27 import svn.client
28 28 import svn.core
29 29 import svn.ra
30 30 import svn.delta
31 31 import transport
32 32 import warnings
33 33 warnings.filterwarnings('ignore',
34 34 module='svn.core',
35 35 category=DeprecationWarning)
36 36
37 37 except ImportError:
38 38 svn = None
39 39
40 40 class SvnPathNotFound(Exception):
41 41 pass
42 42
43 43 def revsplit(rev):
44 44 """Parse a revision string and return (uuid, path, revnum).
45 45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
46 46 ... '/proj%20B/mytrunk/mytrunk@1')
47 47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
48 48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
49 49 ('', '', 1)
50 50 >>> revsplit('@7')
51 51 ('', '', 7)
52 52 >>> revsplit('7')
53 53 ('', '', 0)
54 54 >>> revsplit('bad')
55 55 ('', '', 0)
56 56 """
57 57 parts = rev.rsplit('@', 1)
58 58 revnum = 0
59 59 if len(parts) > 1:
60 60 revnum = int(parts[1])
61 61 parts = parts[0].split('/', 1)
62 62 uuid = ''
63 63 mod = ''
64 64 if len(parts) > 1 and parts[0].startswith('svn:'):
65 65 uuid = parts[0][4:]
66 66 mod = '/' + parts[1]
67 67 return uuid, mod, revnum
68 68
69 69 def quote(s):
70 70 # As of svn 1.7, many svn calls expect "canonical" paths. In
71 71 # theory, we should call svn.core.*canonicalize() on all paths
72 72 # before passing them to the API. Instead, we assume the base url
73 73 # is canonical and copy the behaviour of svn URL encoding function
74 74 # so we can extend it safely with new components. The "safe"
75 75 # characters were taken from the "svn_uri__char_validity" table in
76 76 # libsvn_subr/path.c.
77 77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
78 78
79 79 def geturl(path):
80 80 try:
81 81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
82 82 except SubversionException:
83 83 # svn.client.url_from_path() fails with local repositories
84 84 pass
85 85 if os.path.isdir(path):
86 86 path = os.path.normpath(os.path.abspath(path))
87 87 if os.name == 'nt':
88 88 path = '/' + util.normpath(path)
89 89 # Module URL is later compared with the repository URL returned
90 90 # by svn API, which is UTF-8.
91 91 path = encoding.tolocal(path)
92 92 path = 'file://%s' % quote(path)
93 93 return svn.core.svn_path_canonicalize(path)
94 94
95 95 def optrev(number):
96 96 optrev = svn.core.svn_opt_revision_t()
97 97 optrev.kind = svn.core.svn_opt_revision_number
98 98 optrev.value.number = number
99 99 return optrev
100 100
101 101 class changedpath(object):
102 102 def __init__(self, p):
103 103 self.copyfrom_path = p.copyfrom_path
104 104 self.copyfrom_rev = p.copyfrom_rev
105 105 self.action = p.action
106 106
107 107 def get_log_child(fp, url, paths, start, end, limit=0,
108 108 discover_changed_paths=True, strict_node_history=False):
109 109 protocol = -1
110 110 def receiver(orig_paths, revnum, author, date, message, pool):
111 111 paths = {}
112 112 if orig_paths is not None:
113 113 for k, v in orig_paths.iteritems():
114 114 paths[k] = changedpath(v)
115 115 pickle.dump((paths, revnum, author, date, message),
116 116 fp, protocol)
117 117
118 118 try:
119 119 # Use an ra of our own so that our parent can consume
120 120 # our results without confusing the server.
121 121 t = transport.SvnRaTransport(url=url)
122 122 svn.ra.get_log(t.ra, paths, start, end, limit,
123 123 discover_changed_paths,
124 124 strict_node_history,
125 125 receiver)
126 126 except IOError:
127 127 # Caller may interrupt the iteration
128 128 pickle.dump(None, fp, protocol)
129 129 except Exception, inst:
130 130 pickle.dump(str(inst), fp, protocol)
131 131 else:
132 132 pickle.dump(None, fp, protocol)
133 133 fp.close()
134 134 # With large history, cleanup process goes crazy and suddenly
135 135 # consumes *huge* amount of memory. The output file being closed,
136 136 # there is no need for clean termination.
137 137 os._exit(0)
138 138
139 139 def debugsvnlog(ui, **opts):
140 140 """Fetch SVN log in a subprocess and channel them back to parent to
141 141 avoid memory collection issues.
142 142 """
143 143 if svn is None:
144 144 raise util.Abort(_('debugsvnlog could not load Subversion python '
145 145 'bindings'))
146 146
147 147 util.setbinary(sys.stdin)
148 148 util.setbinary(sys.stdout)
149 149 args = decodeargs(sys.stdin.read())
150 150 get_log_child(sys.stdout, *args)
151 151
152 152 class logstream(object):
153 153 """Interruptible revision log iterator."""
154 154 def __init__(self, stdout):
155 155 self._stdout = stdout
156 156
157 157 def __iter__(self):
158 158 while True:
159 159 try:
160 160 entry = pickle.load(self._stdout)
161 161 except EOFError:
162 162 raise util.Abort(_('Mercurial failed to run itself, check'
163 163 ' hg executable is in PATH'))
164 164 try:
165 165 orig_paths, revnum, author, date, message = entry
166 166 except (TypeError, ValueError):
167 167 if entry is None:
168 168 break
169 169 raise util.Abort(_("log stream exception '%s'") % entry)
170 170 yield entry
171 171
172 172 def close(self):
173 173 if self._stdout:
174 174 self._stdout.close()
175 175 self._stdout = None
176 176
177 177 class directlogstream(list):
178 178 """Direct revision log iterator.
179 179 This can be used for debugging and development but it will probably leak
180 180 memory and is not suitable for real conversions."""
181 181 def __init__(self, url, paths, start, end, limit=0,
182 182 discover_changed_paths=True, strict_node_history=False):
183 183
184 184 def receiver(orig_paths, revnum, author, date, message, pool):
185 185 paths = {}
186 186 if orig_paths is not None:
187 187 for k, v in orig_paths.iteritems():
188 188 paths[k] = changedpath(v)
189 189 self.append((paths, revnum, author, date, message))
190 190
191 191 # Use an ra of our own so that our parent can consume
192 192 # our results without confusing the server.
193 193 t = transport.SvnRaTransport(url=url)
194 194 svn.ra.get_log(t.ra, paths, start, end, limit,
195 195 discover_changed_paths,
196 196 strict_node_history,
197 197 receiver)
198 198
199 199 def close(self):
200 200 pass
201 201
202 202 # Check to see if the given path is a local Subversion repo. Verify this by
203 203 # looking for several svn-specific files and directories in the given
204 204 # directory.
205 205 def filecheck(ui, path, proto):
206 206 for x in ('locks', 'hooks', 'format', 'db'):
207 207 if not os.path.exists(os.path.join(path, x)):
208 208 return False
209 209 return True
210 210
211 211 # Check to see if a given path is the root of an svn repo over http. We verify
212 212 # this by requesting a version-controlled URL we know can't exist and looking
213 213 # for the svn-specific "not found" XML.
214 214 def httpcheck(ui, path, proto):
215 215 try:
216 216 opener = urllib2.build_opener()
217 217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
218 218 data = rsp.read()
219 219 except urllib2.HTTPError, inst:
220 220 if inst.code != 404:
221 221 # Except for 404 we cannot know for sure this is not an svn repo
222 222 ui.warn(_('svn: cannot probe remote repository, assume it could '
223 223 'be a subversion repository. Use --source-type if you '
224 224 'know better.\n'))
225 225 return True
226 226 data = inst.fp.read()
227 227 except Exception:
228 228 # Could be urllib2.URLError if the URL is invalid or anything else.
229 229 return False
230 230 return '<m:human-readable errcode="160013">' in data
231 231
232 232 protomap = {'http': httpcheck,
233 233 'https': httpcheck,
234 234 'file': filecheck,
235 235 }
236 236 def issvnurl(ui, url):
237 237 try:
238 238 proto, path = url.split('://', 1)
239 239 if proto == 'file':
240 240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
241 241 and path[2:6].lower() == '%3a/'):
242 242 path = path[:2] + ':/' + path[6:]
243 243 path = urllib.url2pathname(path)
244 244 except ValueError:
245 245 proto = 'file'
246 246 path = os.path.abspath(url)
247 247 if proto == 'file':
248 248 path = util.pconvert(path)
249 249 check = protomap.get(proto, lambda *args: False)
250 250 while '/' in path:
251 251 if check(ui, path, proto):
252 252 return True
253 253 path = path.rsplit('/', 1)[0]
254 254 return False
255 255
256 256 # SVN conversion code stolen from bzr-svn and tailor
257 257 #
258 258 # Subversion looks like a versioned filesystem, branches structures
259 259 # are defined by conventions and not enforced by the tool. First,
260 260 # we define the potential branches (modules) as "trunk" and "branches"
261 261 # children directories. Revisions are then identified by their
262 262 # module and revision number (and a repository identifier).
263 263 #
264 264 # The revision graph is really a tree (or a forest). By default, a
265 265 # revision parent is the previous revision in the same module. If the
266 266 # module directory is copied/moved from another module then the
267 267 # revision is the module root and its parent the source revision in
268 268 # the parent module. A revision has at most one parent.
269 269 #
270 270 class svn_source(converter_source):
271 271 def __init__(self, ui, url, rev=None):
272 272 super(svn_source, self).__init__(ui, url, rev=rev)
273 273
274 274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
275 275 (os.path.exists(url) and
276 276 os.path.exists(os.path.join(url, '.svn'))) or
277 277 issvnurl(ui, url)):
278 278 raise NoRepo(_("%s does not look like a Subversion repository")
279 279 % url)
280 280 if svn is None:
281 281 raise MissingTool(_('could not load Subversion python bindings'))
282 282
283 283 try:
284 284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
285 285 if version < (1, 4):
286 286 raise MissingTool(_('Subversion python bindings %d.%d found, '
287 287 '1.4 or later required') % version)
288 288 except AttributeError:
289 289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
290 290 'or later required'))
291 291
292 292 self.lastrevs = {}
293 293
294 294 latest = None
295 295 try:
296 296 # Support file://path@rev syntax. Useful e.g. to convert
297 297 # deleted branches.
298 298 at = url.rfind('@')
299 299 if at >= 0:
300 300 latest = int(url[at + 1:])
301 301 url = url[:at]
302 302 except ValueError:
303 303 pass
304 304 self.url = geturl(url)
305 305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
306 306 try:
307 307 self.transport = transport.SvnRaTransport(url=self.url)
308 308 self.ra = self.transport.ra
309 309 self.ctx = self.transport.client
310 310 self.baseurl = svn.ra.get_repos_root(self.ra)
311 311 # Module is either empty or a repository path starting with
312 312 # a slash and not ending with a slash.
313 313 self.module = urllib.unquote(self.url[len(self.baseurl):])
314 314 self.prevmodule = None
315 315 self.rootmodule = self.module
316 316 self.commits = {}
317 317 self.paths = {}
318 318 self.uuid = svn.ra.get_uuid(self.ra)
319 319 except SubversionException:
320 320 ui.traceback()
321 321 raise NoRepo(_("%s does not look like a Subversion repository")
322 322 % self.url)
323 323
324 324 if rev:
325 325 try:
326 326 latest = int(rev)
327 327 except ValueError:
328 328 raise util.Abort(_('svn: revision %s is not an integer') % rev)
329 329
330 330 self.trunkname = self.ui.config('convert', 'svn.trunk',
331 331 'trunk').strip('/')
332 332 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
333 333 try:
334 334 self.startrev = int(self.startrev)
335 335 if self.startrev < 0:
336 336 self.startrev = 0
337 337 except ValueError:
338 338 raise util.Abort(_('svn: start revision %s is not an integer')
339 339 % self.startrev)
340 340
341 341 try:
342 342 self.head = self.latest(self.module, latest)
343 343 except SvnPathNotFound:
344 344 self.head = None
345 345 if not self.head:
346 346 raise util.Abort(_('no revision found in module %s')
347 347 % self.module)
348 348 self.last_changed = self.revnum(self.head)
349 349
350 350 self._changescache = (None, None)
351 351
352 352 if os.path.exists(os.path.join(url, '.svn/entries')):
353 353 self.wc = url
354 354 else:
355 355 self.wc = None
356 356 self.convertfp = None
357 357
358 358 def setrevmap(self, revmap):
359 359 lastrevs = {}
360 360 for revid in revmap.iterkeys():
361 361 uuid, module, revnum = revsplit(revid)
362 362 lastrevnum = lastrevs.setdefault(module, revnum)
363 363 if revnum > lastrevnum:
364 364 lastrevs[module] = revnum
365 365 self.lastrevs = lastrevs
366 366
367 367 def exists(self, path, optrev):
368 368 try:
369 369 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
370 370 optrev, False, self.ctx)
371 371 return True
372 372 except SubversionException:
373 373 return False
374 374
375 375 def getheads(self):
376 376
377 377 def isdir(path, revnum):
378 378 kind = self._checkpath(path, revnum)
379 379 return kind == svn.core.svn_node_dir
380 380
381 381 def getcfgpath(name, rev):
382 382 cfgpath = self.ui.config('convert', 'svn.' + name)
383 383 if cfgpath is not None and cfgpath.strip() == '':
384 384 return None
385 385 path = (cfgpath or name).strip('/')
386 386 if not self.exists(path, rev):
387 387 if self.module.endswith(path) and name == 'trunk':
388 388 # we are converting from inside this directory
389 389 return None
390 390 if cfgpath:
391 391 raise util.Abort(_('expected %s to be at %r, but not found')
392 392 % (name, path))
393 393 return None
394 394 self.ui.note(_('found %s at %r\n') % (name, path))
395 395 return path
396 396
397 397 rev = optrev(self.last_changed)
398 398 oldmodule = ''
399 399 trunk = getcfgpath('trunk', rev)
400 400 self.tags = getcfgpath('tags', rev)
401 401 branches = getcfgpath('branches', rev)
402 402
403 403 # If the project has a trunk or branches, we will extract heads
404 404 # from them. We keep the project root otherwise.
405 405 if trunk:
406 406 oldmodule = self.module or ''
407 407 self.module += '/' + trunk
408 408 self.head = self.latest(self.module, self.last_changed)
409 409 if not self.head:
410 410 raise util.Abort(_('no revision found in module %s')
411 411 % self.module)
412 412
413 413 # First head in the list is the module's head
414 414 self.heads = [self.head]
415 415 if self.tags is not None:
416 416 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
417 417
418 418 # Check if branches bring a few more heads to the list
419 419 if branches:
420 420 rpath = self.url.strip('/')
421 421 branchnames = svn.client.ls(rpath + '/' + quote(branches),
422 422 rev, False, self.ctx)
423 423 for branch in sorted(branchnames):
424 424 module = '%s/%s/%s' % (oldmodule, branches, branch)
425 425 if not isdir(module, self.last_changed):
426 426 continue
427 427 brevid = self.latest(module, self.last_changed)
428 428 if not brevid:
429 429 self.ui.note(_('ignoring empty branch %s\n') % branch)
430 430 continue
431 431 self.ui.note(_('found branch %s at %d\n') %
432 432 (branch, self.revnum(brevid)))
433 433 self.heads.append(brevid)
434 434
435 435 if self.startrev and self.heads:
436 436 if len(self.heads) > 1:
437 437 raise util.Abort(_('svn: start revision is not supported '
438 438 'with more than one branch'))
439 439 revnum = self.revnum(self.heads[0])
440 440 if revnum < self.startrev:
441 441 raise util.Abort(
442 442 _('svn: no revision found after start revision %d')
443 443 % self.startrev)
444 444
445 445 return self.heads
446 446
447 447 def _getchanges(self, rev, full):
448 448 (paths, parents) = self.paths[rev]
449 449 copies = {}
450 450 if parents:
451 451 files, self.removed, copies = self.expandpaths(rev, paths, parents)
452 452 if full or not parents:
453 453 # Perform a full checkout on roots
454 454 uuid, module, revnum = revsplit(rev)
455 455 entries = svn.client.ls(self.baseurl + quote(module),
456 456 optrev(revnum), True, self.ctx)
457 457 files = [n for n, e in entries.iteritems()
458 458 if e.kind == svn.core.svn_node_file]
459 459 self.removed = set()
460 460
461 461 files.sort()
462 462 files = zip(files, [rev] * len(files))
463 463 return (files, copies)
464 464
465 465 def getchanges(self, rev, full):
466 466 # reuse cache from getchangedfiles
467 467 if self._changescache[0] == rev and not full:
468 468 (files, copies) = self._changescache[1]
469 469 else:
470 470 (files, copies) = self._getchanges(rev, full)
471 471 # caller caches the result, so free it here to release memory
472 472 del self.paths[rev]
473 473 return (files, copies)
474 474
475 475 def getchangedfiles(self, rev, i):
476 476 # called from filemap - cache computed values for reuse in getchanges
477 477 (files, copies) = self._getchanges(rev, False)
478 478 self._changescache = (rev, (files, copies))
479 479 return [f[0] for f in files]
480 480
481 481 def getcommit(self, rev):
482 482 if rev not in self.commits:
483 483 uuid, module, revnum = revsplit(rev)
484 484 self.module = module
485 485 self.reparent(module)
486 486 # We assume that:
487 487 # - requests for revisions after "stop" come from the
488 488 # revision graph backward traversal. Cache all of them
489 489 # down to stop, they will be used eventually.
490 490 # - requests for revisions before "stop" come to get
491 491 # isolated branches parents. Just fetch what is needed.
492 492 stop = self.lastrevs.get(module, 0)
493 493 if revnum < stop:
494 494 stop = revnum + 1
495 495 self._fetch_revisions(revnum, stop)
496 496 if rev not in self.commits:
497 497 raise util.Abort(_('svn: revision %s not found') % revnum)
498 498 revcommit = self.commits[rev]
499 499 # caller caches the result, so free it here to release memory
500 500 del self.commits[rev]
501 501 return revcommit
502 502
503 503 def checkrevformat(self, revstr, mapname='splicemap'):
504 504 """ fails if revision format does not match the correct format"""
505 505 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
506 506 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
507 507 '{12,12}(.*)\@[0-9]+$',revstr):
508 508 raise util.Abort(_('%s entry %s is not a valid revision'
509 509 ' identifier') % (mapname, revstr))
510 510
511 def numcommits(self):
512 return int(self.head.rsplit('@', 1)[1]) - self.startrev
513
511 514 def gettags(self):
512 515 tags = {}
513 516 if self.tags is None:
514 517 return tags
515 518
516 519 # svn tags are just a convention, project branches left in a
517 520 # 'tags' directory. There is no other relationship than
518 521 # ancestry, which is expensive to discover and makes them hard
519 522 # to update incrementally. Worse, past revisions may be
520 523 # referenced by tags far away in the future, requiring a deep
521 524 # history traversal on every calculation. Current code
522 525 # performs a single backward traversal, tracking moves within
523 526 # the tags directory (tag renaming) and recording a new tag
524 527 # everytime a project is copied from outside the tags
525 528 # directory. It also lists deleted tags, this behaviour may
526 529 # change in the future.
527 530 pendings = []
528 531 tagspath = self.tags
529 532 start = svn.ra.get_latest_revnum(self.ra)
530 533 stream = self._getlog([self.tags], start, self.startrev)
531 534 try:
532 535 for entry in stream:
533 536 origpaths, revnum, author, date, message = entry
534 537 if not origpaths:
535 538 origpaths = []
536 539 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
537 540 in origpaths.iteritems() if e.copyfrom_path]
538 541 # Apply moves/copies from more specific to general
539 542 copies.sort(reverse=True)
540 543
541 544 srctagspath = tagspath
542 545 if copies and copies[-1][2] == tagspath:
543 546 # Track tags directory moves
544 547 srctagspath = copies.pop()[0]
545 548
546 549 for source, sourcerev, dest in copies:
547 550 if not dest.startswith(tagspath + '/'):
548 551 continue
549 552 for tag in pendings:
550 553 if tag[0].startswith(dest):
551 554 tagpath = source + tag[0][len(dest):]
552 555 tag[:2] = [tagpath, sourcerev]
553 556 break
554 557 else:
555 558 pendings.append([source, sourcerev, dest])
556 559
557 560 # Filter out tags with children coming from different
558 561 # parts of the repository like:
559 562 # /tags/tag.1 (from /trunk:10)
560 563 # /tags/tag.1/foo (from /branches/foo:12)
561 564 # Here/tags/tag.1 discarded as well as its children.
562 565 # It happens with tools like cvs2svn. Such tags cannot
563 566 # be represented in mercurial.
564 567 addeds = dict((p, e.copyfrom_path) for p, e
565 568 in origpaths.iteritems()
566 569 if e.action == 'A' and e.copyfrom_path)
567 570 badroots = set()
568 571 for destroot in addeds:
569 572 for source, sourcerev, dest in pendings:
570 573 if (not dest.startswith(destroot + '/')
571 574 or source.startswith(addeds[destroot] + '/')):
572 575 continue
573 576 badroots.add(destroot)
574 577 break
575 578
576 579 for badroot in badroots:
577 580 pendings = [p for p in pendings if p[2] != badroot
578 581 and not p[2].startswith(badroot + '/')]
579 582
580 583 # Tell tag renamings from tag creations
581 584 renamings = []
582 585 for source, sourcerev, dest in pendings:
583 586 tagname = dest.split('/')[-1]
584 587 if source.startswith(srctagspath):
585 588 renamings.append([source, sourcerev, tagname])
586 589 continue
587 590 if tagname in tags:
588 591 # Keep the latest tag value
589 592 continue
590 593 # From revision may be fake, get one with changes
591 594 try:
592 595 tagid = self.latest(source, sourcerev)
593 596 if tagid and tagname not in tags:
594 597 tags[tagname] = tagid
595 598 except SvnPathNotFound:
596 599 # It happens when we are following directories
597 600 # we assumed were copied with their parents
598 601 # but were really created in the tag
599 602 # directory.
600 603 pass
601 604 pendings = renamings
602 605 tagspath = srctagspath
603 606 finally:
604 607 stream.close()
605 608 return tags
606 609
607 610 def converted(self, rev, destrev):
608 611 if not self.wc:
609 612 return
610 613 if self.convertfp is None:
611 614 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
612 615 'a')
613 616 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
614 617 self.convertfp.flush()
615 618
616 619 def revid(self, revnum, module=None):
617 620 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
618 621
619 622 def revnum(self, rev):
620 623 return int(rev.split('@')[-1])
621 624
622 625 def latest(self, path, stop=None):
623 626 """Find the latest revid affecting path, up to stop revision
624 627 number. If stop is None, default to repository latest
625 628 revision. It may return a revision in a different module,
626 629 since a branch may be moved without a change being
627 630 reported. Return None if computed module does not belong to
628 631 rootmodule subtree.
629 632 """
630 633 def findchanges(path, start, stop=None):
631 634 stream = self._getlog([path], start, stop or 1)
632 635 try:
633 636 for entry in stream:
634 637 paths, revnum, author, date, message = entry
635 638 if stop is None and paths:
636 639 # We do not know the latest changed revision,
637 640 # keep the first one with changed paths.
638 641 break
639 642 if revnum <= stop:
640 643 break
641 644
642 645 for p in paths:
643 646 if (not path.startswith(p) or
644 647 not paths[p].copyfrom_path):
645 648 continue
646 649 newpath = paths[p].copyfrom_path + path[len(p):]
647 650 self.ui.debug("branch renamed from %s to %s at %d\n" %
648 651 (path, newpath, revnum))
649 652 path = newpath
650 653 break
651 654 if not paths:
652 655 revnum = None
653 656 return revnum, path
654 657 finally:
655 658 stream.close()
656 659
657 660 if not path.startswith(self.rootmodule):
658 661 # Requests on foreign branches may be forbidden at server level
659 662 self.ui.debug('ignoring foreign branch %r\n' % path)
660 663 return None
661 664
662 665 if stop is None:
663 666 stop = svn.ra.get_latest_revnum(self.ra)
664 667 try:
665 668 prevmodule = self.reparent('')
666 669 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
667 670 self.reparent(prevmodule)
668 671 except SubversionException:
669 672 dirent = None
670 673 if not dirent:
671 674 raise SvnPathNotFound(_('%s not found up to revision %d')
672 675 % (path, stop))
673 676
674 677 # stat() gives us the previous revision on this line of
675 678 # development, but it might be in *another module*. Fetch the
676 679 # log and detect renames down to the latest revision.
677 680 revnum, realpath = findchanges(path, stop, dirent.created_rev)
678 681 if revnum is None:
679 682 # Tools like svnsync can create empty revision, when
680 683 # synchronizing only a subtree for instance. These empty
681 684 # revisions created_rev still have their original values
682 685 # despite all changes having disappeared and can be
683 686 # returned by ra.stat(), at least when stating the root
684 687 # module. In that case, do not trust created_rev and scan
685 688 # the whole history.
686 689 revnum, realpath = findchanges(path, stop)
687 690 if revnum is None:
688 691 self.ui.debug('ignoring empty branch %r\n' % realpath)
689 692 return None
690 693
691 694 if not realpath.startswith(self.rootmodule):
692 695 self.ui.debug('ignoring foreign branch %r\n' % realpath)
693 696 return None
694 697 return self.revid(revnum, realpath)
695 698
696 699 def reparent(self, module):
697 700 """Reparent the svn transport and return the previous parent."""
698 701 if self.prevmodule == module:
699 702 return module
700 703 svnurl = self.baseurl + quote(module)
701 704 prevmodule = self.prevmodule
702 705 if prevmodule is None:
703 706 prevmodule = ''
704 707 self.ui.debug("reparent to %s\n" % svnurl)
705 708 svn.ra.reparent(self.ra, svnurl)
706 709 self.prevmodule = module
707 710 return prevmodule
708 711
709 712 def expandpaths(self, rev, paths, parents):
710 713 changed, removed = set(), set()
711 714 copies = {}
712 715
713 716 new_module, revnum = revsplit(rev)[1:]
714 717 if new_module != self.module:
715 718 self.module = new_module
716 719 self.reparent(self.module)
717 720
718 721 for i, (path, ent) in enumerate(paths):
719 722 self.ui.progress(_('scanning paths'), i, item=path,
720 723 total=len(paths))
721 724 entrypath = self.getrelpath(path)
722 725
723 726 kind = self._checkpath(entrypath, revnum)
724 727 if kind == svn.core.svn_node_file:
725 728 changed.add(self.recode(entrypath))
726 729 if not ent.copyfrom_path or not parents:
727 730 continue
728 731 # Copy sources not in parent revisions cannot be
729 732 # represented, ignore their origin for now
730 733 pmodule, prevnum = revsplit(parents[0])[1:]
731 734 if ent.copyfrom_rev < prevnum:
732 735 continue
733 736 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
734 737 if not copyfrom_path:
735 738 continue
736 739 self.ui.debug("copied to %s from %s@%s\n" %
737 740 (entrypath, copyfrom_path, ent.copyfrom_rev))
738 741 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
739 742 elif kind == 0: # gone, but had better be a deleted *file*
740 743 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
741 744 pmodule, prevnum = revsplit(parents[0])[1:]
742 745 parentpath = pmodule + "/" + entrypath
743 746 fromkind = self._checkpath(entrypath, prevnum, pmodule)
744 747
745 748 if fromkind == svn.core.svn_node_file:
746 749 removed.add(self.recode(entrypath))
747 750 elif fromkind == svn.core.svn_node_dir:
748 751 oroot = parentpath.strip('/')
749 752 nroot = path.strip('/')
750 753 children = self._iterfiles(oroot, prevnum)
751 754 for childpath in children:
752 755 childpath = childpath.replace(oroot, nroot)
753 756 childpath = self.getrelpath("/" + childpath, pmodule)
754 757 if childpath:
755 758 removed.add(self.recode(childpath))
756 759 else:
757 760 self.ui.debug('unknown path in revision %d: %s\n' % \
758 761 (revnum, path))
759 762 elif kind == svn.core.svn_node_dir:
760 763 if ent.action == 'M':
761 764 # If the directory just had a prop change,
762 765 # then we shouldn't need to look for its children.
763 766 continue
764 767 if ent.action == 'R' and parents:
765 768 # If a directory is replacing a file, mark the previous
766 769 # file as deleted
767 770 pmodule, prevnum = revsplit(parents[0])[1:]
768 771 pkind = self._checkpath(entrypath, prevnum, pmodule)
769 772 if pkind == svn.core.svn_node_file:
770 773 removed.add(self.recode(entrypath))
771 774 elif pkind == svn.core.svn_node_dir:
772 775 # We do not know what files were kept or removed,
773 776 # mark them all as changed.
774 777 for childpath in self._iterfiles(pmodule, prevnum):
775 778 childpath = self.getrelpath("/" + childpath)
776 779 if childpath:
777 780 changed.add(self.recode(childpath))
778 781
779 782 for childpath in self._iterfiles(path, revnum):
780 783 childpath = self.getrelpath("/" + childpath)
781 784 if childpath:
782 785 changed.add(self.recode(childpath))
783 786
784 787 # Handle directory copies
785 788 if not ent.copyfrom_path or not parents:
786 789 continue
787 790 # Copy sources not in parent revisions cannot be
788 791 # represented, ignore their origin for now
789 792 pmodule, prevnum = revsplit(parents[0])[1:]
790 793 if ent.copyfrom_rev < prevnum:
791 794 continue
792 795 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
793 796 if not copyfrompath:
794 797 continue
795 798 self.ui.debug("mark %s came from %s:%d\n"
796 799 % (path, copyfrompath, ent.copyfrom_rev))
797 800 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
798 801 for childpath in children:
799 802 childpath = self.getrelpath("/" + childpath, pmodule)
800 803 if not childpath:
801 804 continue
802 805 copytopath = path + childpath[len(copyfrompath):]
803 806 copytopath = self.getrelpath(copytopath)
804 807 copies[self.recode(copytopath)] = self.recode(childpath)
805 808
806 809 self.ui.progress(_('scanning paths'), None)
807 810 changed.update(removed)
808 811 return (list(changed), removed, copies)
809 812
810 813 def _fetch_revisions(self, from_revnum, to_revnum):
811 814 if from_revnum < to_revnum:
812 815 from_revnum, to_revnum = to_revnum, from_revnum
813 816
814 817 self.child_cset = None
815 818
816 819 def parselogentry(orig_paths, revnum, author, date, message):
817 820 """Return the parsed commit object or None, and True if
818 821 the revision is a branch root.
819 822 """
820 823 self.ui.debug("parsing revision %d (%d changes)\n" %
821 824 (revnum, len(orig_paths)))
822 825
823 826 branched = False
824 827 rev = self.revid(revnum)
825 828 # branch log might return entries for a parent we already have
826 829
827 830 if rev in self.commits or revnum < to_revnum:
828 831 return None, branched
829 832
830 833 parents = []
831 834 # check whether this revision is the start of a branch or part
832 835 # of a branch renaming
833 836 orig_paths = sorted(orig_paths.iteritems())
834 837 root_paths = [(p, e) for p, e in orig_paths
835 838 if self.module.startswith(p)]
836 839 if root_paths:
837 840 path, ent = root_paths[-1]
838 841 if ent.copyfrom_path:
839 842 branched = True
840 843 newpath = ent.copyfrom_path + self.module[len(path):]
841 844 # ent.copyfrom_rev may not be the actual last revision
842 845 previd = self.latest(newpath, ent.copyfrom_rev)
843 846 if previd is not None:
844 847 prevmodule, prevnum = revsplit(previd)[1:]
845 848 if prevnum >= self.startrev:
846 849 parents = [previd]
847 850 self.ui.note(
848 851 _('found parent of branch %s at %d: %s\n') %
849 852 (self.module, prevnum, prevmodule))
850 853 else:
851 854 self.ui.debug("no copyfrom path, don't know what to do.\n")
852 855
853 856 paths = []
854 857 # filter out unrelated paths
855 858 for path, ent in orig_paths:
856 859 if self.getrelpath(path) is None:
857 860 continue
858 861 paths.append((path, ent))
859 862
860 863 # Example SVN datetime. Includes microseconds.
861 864 # ISO-8601 conformant
862 865 # '2007-01-04T17:35:00.902377Z'
863 866 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
864 867 if self.ui.configbool('convert', 'localtimezone'):
865 868 date = makedatetimestamp(date[0])
866 869
867 870 log = message and self.recode(message) or ''
868 871 author = author and self.recode(author) or ''
869 872 try:
870 873 branch = self.module.split("/")[-1]
871 874 if branch == self.trunkname:
872 875 branch = None
873 876 except IndexError:
874 877 branch = None
875 878
876 879 cset = commit(author=author,
877 880 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
878 881 desc=log,
879 882 parents=parents,
880 883 branch=branch,
881 884 rev=rev)
882 885
883 886 self.commits[rev] = cset
884 887 # The parents list is *shared* among self.paths and the
885 888 # commit object. Both will be updated below.
886 889 self.paths[rev] = (paths, cset.parents)
887 890 if self.child_cset and not self.child_cset.parents:
888 891 self.child_cset.parents[:] = [rev]
889 892 self.child_cset = cset
890 893 return cset, branched
891 894
892 895 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
893 896 (self.module, from_revnum, to_revnum))
894 897
895 898 try:
896 899 firstcset = None
897 900 lastonbranch = False
898 901 stream = self._getlog([self.module], from_revnum, to_revnum)
899 902 try:
900 903 for entry in stream:
901 904 paths, revnum, author, date, message = entry
902 905 if revnum < self.startrev:
903 906 lastonbranch = True
904 907 break
905 908 if not paths:
906 909 self.ui.debug('revision %d has no entries\n' % revnum)
907 910 # If we ever leave the loop on an empty
908 911 # revision, do not try to get a parent branch
909 912 lastonbranch = lastonbranch or revnum == 0
910 913 continue
911 914 cset, lastonbranch = parselogentry(paths, revnum, author,
912 915 date, message)
913 916 if cset:
914 917 firstcset = cset
915 918 if lastonbranch:
916 919 break
917 920 finally:
918 921 stream.close()
919 922
920 923 if not lastonbranch and firstcset and not firstcset.parents:
921 924 # The first revision of the sequence (the last fetched one)
922 925 # has invalid parents if not a branch root. Find the parent
923 926 # revision now, if any.
924 927 try:
925 928 firstrevnum = self.revnum(firstcset.rev)
926 929 if firstrevnum > 1:
927 930 latest = self.latest(self.module, firstrevnum - 1)
928 931 if latest:
929 932 firstcset.parents.append(latest)
930 933 except SvnPathNotFound:
931 934 pass
932 935 except SubversionException, (inst, num):
933 936 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
934 937 raise util.Abort(_('svn: branch has no revision %s')
935 938 % to_revnum)
936 939 raise
937 940
938 941 def getfile(self, file, rev):
939 942 # TODO: ra.get_file transmits the whole file instead of diffs.
940 943 if file in self.removed:
941 944 return None, None
942 945 mode = ''
943 946 try:
944 947 new_module, revnum = revsplit(rev)[1:]
945 948 if self.module != new_module:
946 949 self.module = new_module
947 950 self.reparent(self.module)
948 951 io = StringIO()
949 952 info = svn.ra.get_file(self.ra, file, revnum, io)
950 953 data = io.getvalue()
951 954 # ra.get_file() seems to keep a reference on the input buffer
952 955 # preventing collection. Release it explicitly.
953 956 io.close()
954 957 if isinstance(info, list):
955 958 info = info[-1]
956 959 mode = ("svn:executable" in info) and 'x' or ''
957 960 mode = ("svn:special" in info) and 'l' or mode
958 961 except SubversionException, e:
959 962 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
960 963 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
961 964 if e.apr_err in notfound: # File not found
962 965 return None, None
963 966 raise
964 967 if mode == 'l':
965 968 link_prefix = "link "
966 969 if data.startswith(link_prefix):
967 970 data = data[len(link_prefix):]
968 971 return data, mode
969 972
970 973 def _iterfiles(self, path, revnum):
971 974 """Enumerate all files in path at revnum, recursively."""
972 975 path = path.strip('/')
973 976 pool = Pool()
974 977 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
975 978 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
976 979 if path:
977 980 path += '/'
978 981 return ((path + p) for p, e in entries.iteritems()
979 982 if e.kind == svn.core.svn_node_file)
980 983
981 984 def getrelpath(self, path, module=None):
982 985 if module is None:
983 986 module = self.module
984 987 # Given the repository url of this wc, say
985 988 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
986 989 # extract the "entry" portion (a relative path) from what
987 990 # svn log --xml says, i.e.
988 991 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
989 992 # that is to say "tests/PloneTestCase.py"
990 993 if path.startswith(module):
991 994 relative = path.rstrip('/')[len(module):]
992 995 if relative.startswith('/'):
993 996 return relative[1:]
994 997 elif relative == '':
995 998 return relative
996 999
997 1000 # The path is outside our tracked tree...
998 1001 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
999 1002 return None
1000 1003
1001 1004 def _checkpath(self, path, revnum, module=None):
1002 1005 if module is not None:
1003 1006 prevmodule = self.reparent('')
1004 1007 path = module + '/' + path
1005 1008 try:
1006 1009 # ra.check_path does not like leading slashes very much, it leads
1007 1010 # to PROPFIND subversion errors
1008 1011 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1009 1012 finally:
1010 1013 if module is not None:
1011 1014 self.reparent(prevmodule)
1012 1015
1013 1016 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1014 1017 strict_node_history=False):
1015 1018 # Normalize path names, svn >= 1.5 only wants paths relative to
1016 1019 # supplied URL
1017 1020 relpaths = []
1018 1021 for p in paths:
1019 1022 if not p.startswith('/'):
1020 1023 p = self.module + '/' + p
1021 1024 relpaths.append(p.strip('/'))
1022 1025 args = [self.baseurl, relpaths, start, end, limit,
1023 1026 discover_changed_paths, strict_node_history]
1024 1027 # undocumented feature: debugsvnlog can be disabled
1025 1028 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1026 1029 return directlogstream(*args)
1027 1030 arg = encodeargs(args)
1028 1031 hgexe = util.hgexecutable()
1029 1032 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1030 1033 stdin, stdout = util.popen2(util.quotecommand(cmd))
1031 1034 stdin.write(arg)
1032 1035 try:
1033 1036 stdin.close()
1034 1037 except IOError:
1035 1038 raise util.Abort(_('Mercurial failed to run itself, check'
1036 1039 ' hg executable is in PATH'))
1037 1040 return logstream(stdout)
1038 1041
1039 1042 pre_revprop_change = '''#!/bin/sh
1040 1043
1041 1044 REPOS="$1"
1042 1045 REV="$2"
1043 1046 USER="$3"
1044 1047 PROPNAME="$4"
1045 1048 ACTION="$5"
1046 1049
1047 1050 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1048 1051 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1049 1052 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1050 1053
1051 1054 echo "Changing prohibited revision property" >&2
1052 1055 exit 1
1053 1056 '''
1054 1057
1055 1058 class svn_sink(converter_sink, commandline):
1056 1059 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1057 1060 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1058 1061
1059 1062 def prerun(self):
1060 1063 if self.wc:
1061 1064 os.chdir(self.wc)
1062 1065
1063 1066 def postrun(self):
1064 1067 if self.wc:
1065 1068 os.chdir(self.cwd)
1066 1069
1067 1070 def join(self, name):
1068 1071 return os.path.join(self.wc, '.svn', name)
1069 1072
1070 1073 def revmapfile(self):
1071 1074 return self.join('hg-shamap')
1072 1075
1073 1076 def authorfile(self):
1074 1077 return self.join('hg-authormap')
1075 1078
1076 1079 def __init__(self, ui, path):
1077 1080
1078 1081 converter_sink.__init__(self, ui, path)
1079 1082 commandline.__init__(self, ui, 'svn')
1080 1083 self.delete = []
1081 1084 self.setexec = []
1082 1085 self.delexec = []
1083 1086 self.copies = []
1084 1087 self.wc = None
1085 1088 self.cwd = os.getcwd()
1086 1089
1087 1090 created = False
1088 1091 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1089 1092 self.wc = os.path.realpath(path)
1090 1093 self.run0('update')
1091 1094 else:
1092 1095 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1093 1096 path = os.path.realpath(path)
1094 1097 if os.path.isdir(os.path.dirname(path)):
1095 1098 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1096 1099 ui.status(_('initializing svn repository %r\n') %
1097 1100 os.path.basename(path))
1098 1101 commandline(ui, 'svnadmin').run0('create', path)
1099 1102 created = path
1100 1103 path = util.normpath(path)
1101 1104 if not path.startswith('/'):
1102 1105 path = '/' + path
1103 1106 path = 'file://' + path
1104 1107
1105 1108 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1106 1109 ui.status(_('initializing svn working copy %r\n')
1107 1110 % os.path.basename(wcpath))
1108 1111 self.run0('checkout', path, wcpath)
1109 1112
1110 1113 self.wc = wcpath
1111 1114 self.opener = scmutil.opener(self.wc)
1112 1115 self.wopener = scmutil.opener(self.wc)
1113 1116 self.childmap = mapfile(ui, self.join('hg-childmap'))
1114 1117 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1115 1118
1116 1119 if created:
1117 1120 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1118 1121 fp = open(hook, 'w')
1119 1122 fp.write(pre_revprop_change)
1120 1123 fp.close()
1121 1124 util.setflags(hook, False, True)
1122 1125
1123 1126 output = self.run0('info')
1124 1127 self.uuid = self.uuid_re.search(output).group(1).strip()
1125 1128
1126 1129 def wjoin(self, *names):
1127 1130 return os.path.join(self.wc, *names)
1128 1131
1129 1132 @propertycache
1130 1133 def manifest(self):
1131 1134 # As of svn 1.7, the "add" command fails when receiving
1132 1135 # already tracked entries, so we have to track and filter them
1133 1136 # ourselves.
1134 1137 m = set()
1135 1138 output = self.run0('ls', recursive=True, xml=True)
1136 1139 doc = xml.dom.minidom.parseString(output)
1137 1140 for e in doc.getElementsByTagName('entry'):
1138 1141 for n in e.childNodes:
1139 1142 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1140 1143 continue
1141 1144 name = ''.join(c.data for c in n.childNodes
1142 1145 if c.nodeType == c.TEXT_NODE)
1143 1146 # Entries are compared with names coming from
1144 1147 # mercurial, so bytes with undefined encoding. Our
1145 1148 # best bet is to assume they are in local
1146 1149 # encoding. They will be passed to command line calls
1147 1150 # later anyway, so they better be.
1148 1151 m.add(encoding.tolocal(name.encode('utf-8')))
1149 1152 break
1150 1153 return m
1151 1154
1152 1155 def putfile(self, filename, flags, data):
1153 1156 if 'l' in flags:
1154 1157 self.wopener.symlink(data, filename)
1155 1158 else:
1156 1159 try:
1157 1160 if os.path.islink(self.wjoin(filename)):
1158 1161 os.unlink(filename)
1159 1162 except OSError:
1160 1163 pass
1161 1164 self.wopener.write(filename, data)
1162 1165
1163 1166 if self.is_exec:
1164 1167 if self.is_exec(self.wjoin(filename)):
1165 1168 if 'x' not in flags:
1166 1169 self.delexec.append(filename)
1167 1170 else:
1168 1171 if 'x' in flags:
1169 1172 self.setexec.append(filename)
1170 1173 util.setflags(self.wjoin(filename), False, 'x' in flags)
1171 1174
1172 1175 def _copyfile(self, source, dest):
1173 1176 # SVN's copy command pukes if the destination file exists, but
1174 1177 # our copyfile method expects to record a copy that has
1175 1178 # already occurred. Cross the semantic gap.
1176 1179 wdest = self.wjoin(dest)
1177 1180 exists = os.path.lexists(wdest)
1178 1181 if exists:
1179 1182 fd, tempname = tempfile.mkstemp(
1180 1183 prefix='hg-copy-', dir=os.path.dirname(wdest))
1181 1184 os.close(fd)
1182 1185 os.unlink(tempname)
1183 1186 os.rename(wdest, tempname)
1184 1187 try:
1185 1188 self.run0('copy', source, dest)
1186 1189 finally:
1187 1190 self.manifest.add(dest)
1188 1191 if exists:
1189 1192 try:
1190 1193 os.unlink(wdest)
1191 1194 except OSError:
1192 1195 pass
1193 1196 os.rename(tempname, wdest)
1194 1197
1195 1198 def dirs_of(self, files):
1196 1199 dirs = set()
1197 1200 for f in files:
1198 1201 if os.path.isdir(self.wjoin(f)):
1199 1202 dirs.add(f)
1200 1203 for i in strutil.rfindall(f, '/'):
1201 1204 dirs.add(f[:i])
1202 1205 return dirs
1203 1206
1204 1207 def add_dirs(self, files):
1205 1208 add_dirs = [d for d in sorted(self.dirs_of(files))
1206 1209 if d not in self.manifest]
1207 1210 if add_dirs:
1208 1211 self.manifest.update(add_dirs)
1209 1212 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1210 1213 return add_dirs
1211 1214
1212 1215 def add_files(self, files):
1213 1216 files = [f for f in files if f not in self.manifest]
1214 1217 if files:
1215 1218 self.manifest.update(files)
1216 1219 self.xargs(files, 'add', quiet=True)
1217 1220 return files
1218 1221
1219 1222 def addchild(self, parent, child):
1220 1223 self.childmap[parent] = child
1221 1224
1222 1225 def revid(self, rev):
1223 1226 return u"svn:%s@%s" % (self.uuid, rev)
1224 1227
1225 1228 def putcommit(self, files, copies, parents, commit, source, revmap, full):
1226 1229 for parent in parents:
1227 1230 try:
1228 1231 return self.revid(self.childmap[parent])
1229 1232 except KeyError:
1230 1233 pass
1231 1234
1232 1235 # Apply changes to working copy
1233 1236 for f, v in files:
1234 1237 data, mode = source.getfile(f, v)
1235 1238 if data is None:
1236 1239 self.delete.append(f)
1237 1240 else:
1238 1241 self.putfile(f, mode, data)
1239 1242 if f in copies:
1240 1243 self.copies.append([copies[f], f])
1241 1244 if full:
1242 1245 self.delete.extend(sorted(self.manifest.difference(files)))
1243 1246 files = [f[0] for f in files]
1244 1247
1245 1248 entries = set(self.delete)
1246 1249 files = frozenset(files)
1247 1250 entries.update(self.add_dirs(files.difference(entries)))
1248 1251 if self.copies:
1249 1252 for s, d in self.copies:
1250 1253 self._copyfile(s, d)
1251 1254 self.copies = []
1252 1255 if self.delete:
1253 1256 self.xargs(self.delete, 'delete')
1254 1257 for f in self.delete:
1255 1258 self.manifest.remove(f)
1256 1259 self.delete = []
1257 1260 entries.update(self.add_files(files.difference(entries)))
1258 1261 if self.delexec:
1259 1262 self.xargs(self.delexec, 'propdel', 'svn:executable')
1260 1263 self.delexec = []
1261 1264 if self.setexec:
1262 1265 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1263 1266 self.setexec = []
1264 1267
1265 1268 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1266 1269 fp = os.fdopen(fd, 'w')
1267 1270 fp.write(commit.desc)
1268 1271 fp.close()
1269 1272 try:
1270 1273 output = self.run0('commit',
1271 1274 username=util.shortuser(commit.author),
1272 1275 file=messagefile,
1273 1276 encoding='utf-8')
1274 1277 try:
1275 1278 rev = self.commit_re.search(output).group(1)
1276 1279 except AttributeError:
1277 1280 if not files:
1278 1281 return parents[0]
1279 1282 self.ui.warn(_('unexpected svn output:\n'))
1280 1283 self.ui.warn(output)
1281 1284 raise util.Abort(_('unable to cope with svn output'))
1282 1285 if commit.rev:
1283 1286 self.run('propset', 'hg:convert-rev', commit.rev,
1284 1287 revprop=True, revision=rev)
1285 1288 if commit.branch and commit.branch != 'default':
1286 1289 self.run('propset', 'hg:convert-branch', commit.branch,
1287 1290 revprop=True, revision=rev)
1288 1291 for parent in parents:
1289 1292 self.addchild(parent, rev)
1290 1293 return self.revid(rev)
1291 1294 finally:
1292 1295 os.unlink(messagefile)
1293 1296
1294 1297 def puttags(self, tags):
1295 1298 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1296 1299 return None, None
1297 1300
1298 1301 def hascommitfrommap(self, rev):
1299 1302 # We trust that revisions referenced in a map still is present
1300 1303 # TODO: implement something better if necessary and feasible
1301 1304 return True
1302 1305
1303 1306 def hascommitforsplicemap(self, rev):
1304 1307 # This is not correct as one can convert to an existing subversion
1305 1308 # repository and childmap would not list all revisions. Too bad.
1306 1309 if rev in self.childmap:
1307 1310 return True
1308 1311 raise util.Abort(_('splice map revision %s not found in subversion '
1309 1312 'child map (revision lookups are not implemented)')
1310 1313 % rev)
@@ -1,133 +1,133 b''
1 1 #require svn svn-bindings
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > convert =
6 6 > EOF
7 7
8 8 $ svnadmin create svn-repo
9 9 $ svnadmin load -q svn-repo < "$TESTDIR/svn/encoding.svndump"
10 10
11 11 Convert while testing all possible outputs
12 12
13 13 $ hg --debug convert svn-repo A-hg
14 14 initializing destination A-hg repository
15 15 reparent to file://*/svn-repo (glob)
16 16 run hg sink pre-conversion action
17 17 scanning source...
18 18 found trunk at 'trunk'
19 19 found tags at 'tags'
20 20 found branches at 'branches'
21 21 found branch branch\xc3\xa9 at 5 (esc)
22 22 found branch branch\xc3\xa9e at 6 (esc)
23 scanning: 1 revisions
23 scanning: 1/4 revisions (25.00%)
24 24 reparent to file://*/svn-repo/trunk (glob)
25 25 fetching revision log for "/trunk" from 4 to 0
26 26 parsing revision 4 (2 changes)
27 27 parsing revision 3 (4 changes)
28 28 parsing revision 2 (3 changes)
29 29 parsing revision 1 (3 changes)
30 30 no copyfrom path, don't know what to do.
31 31 '/branches' is not under '/trunk', ignoring
32 32 '/tags' is not under '/trunk', ignoring
33 scanning: 2 revisions
33 scanning: 2/4 revisions (50.00%)
34 34 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
35 35 fetching revision log for "/branches/branch\xc3\xa9" from 5 to 0 (esc)
36 36 parsing revision 5 (1 changes)
37 37 reparent to file://*/svn-repo (glob)
38 38 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
39 39 found parent of branch /branches/branch\xc3\xa9 at 4: /trunk (esc)
40 scanning: 3 revisions
40 scanning: 3/4 revisions (75.00%)
41 41 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
42 42 fetching revision log for "/branches/branch\xc3\xa9e" from 6 to 0 (esc)
43 43 parsing revision 6 (1 changes)
44 44 reparent to file://*/svn-repo (glob)
45 45 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
46 46 found parent of branch /branches/branch\xc3\xa9e at 5: /branches/branch\xc3\xa9 (esc)
47 scanning: 4 revisions
48 scanning: 5 revisions
49 scanning: 6 revisions
47 scanning: 4/4 revisions (100.00%)
48 scanning: 5/4 revisions (125.00%)
49 scanning: 6/4 revisions (150.00%)
50 50 sorting...
51 51 converting...
52 52 5 init projA
53 53 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@1
54 54 converting: 0/6 revisions (0.00%)
55 55 4 hello
56 56 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@2
57 57 converting: 1/6 revisions (16.67%)
58 58 reparent to file://*/svn-repo/trunk (glob)
59 59 scanning paths: /trunk/\xc3\xa0 0/3 (0.00%) (esc)
60 60 scanning paths: /trunk/\xc3\xa0/e\xcc\x81 1/3 (33.33%) (esc)
61 61 scanning paths: /trunk/\xc3\xa9 2/3 (66.67%) (esc)
62 62 \xc3\xa0/e\xcc\x81 (esc)
63 63 getting files: \xc3\xa0/e\xcc\x81 1/2 (50.00%) (esc)
64 64 \xc3\xa9 (esc)
65 65 getting files: \xc3\xa9 2/2 (100.00%) (esc)
66 66 3 copy files
67 67 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@3
68 68 converting: 2/6 revisions (33.33%)
69 69 scanning paths: /trunk/\xc3\xa0 0/4 (0.00%) (esc)
70 70 gone from -1
71 71 reparent to file://*/svn-repo (glob)
72 72 reparent to file://*/svn-repo/trunk (glob)
73 73 scanning paths: /trunk/\xc3\xa8 1/4 (25.00%) (esc)
74 74 copied to \xc3\xa8 from \xc3\xa9@2 (esc)
75 75 scanning paths: /trunk/\xc3\xa9 2/4 (50.00%) (esc)
76 76 gone from -1
77 77 reparent to file://*/svn-repo (glob)
78 78 reparent to file://*/svn-repo/trunk (glob)
79 79 scanning paths: /trunk/\xc3\xb9 3/4 (75.00%) (esc)
80 80 mark /trunk/\xc3\xb9 came from \xc3\xa0:2 (esc)
81 81 \xc3\xa0/e\xcc\x81 (esc)
82 82 getting files: \xc3\xa0/e\xcc\x81 1/4 (25.00%) (esc)
83 83 \xc3\xa8 (esc)
84 84 getting files: \xc3\xa8 2/4 (50.00%) (esc)
85 85 \xc3\xa8: copy \xc3\xa9:6b67ccefd5ce6de77e7ead4f5292843a0255329f (esc)
86 86 \xc3\xa9 (esc)
87 87 getting files: \xc3\xa9 3/4 (75.00%) (esc)
88 88 \xc3\xb9/e\xcc\x81 (esc)
89 89 getting files: \xc3\xb9/e\xcc\x81 4/4 (100.00%) (esc)
90 90 \xc3\xb9/e\xcc\x81: copy \xc3\xa0/e\xcc\x81:a9092a3d84a37b9993b5c73576f6de29b7ea50f6 (esc)
91 91 2 remove files
92 92 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@4
93 93 converting: 3/6 revisions (50.00%)
94 94 scanning paths: /trunk/\xc3\xa8 0/2 (0.00%) (esc)
95 95 gone from -1
96 96 reparent to file://*/svn-repo (glob)
97 97 reparent to file://*/svn-repo/trunk (glob)
98 98 scanning paths: /trunk/\xc3\xb9 1/2 (50.00%) (esc)
99 99 gone from -1
100 100 reparent to file://*/svn-repo (glob)
101 101 reparent to file://*/svn-repo/trunk (glob)
102 102 \xc3\xa8 (esc)
103 103 getting files: \xc3\xa8 1/2 (50.00%) (esc)
104 104 \xc3\xb9/e\xcc\x81 (esc)
105 105 getting files: \xc3\xb9/e\xcc\x81 2/2 (100.00%) (esc)
106 106 1 branch to branch?
107 107 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?@5
108 108 converting: 4/6 revisions (66.67%)
109 109 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
110 110 scanning paths: /branches/branch\xc3\xa9 0/1 (0.00%) (esc)
111 111 0 branch to branch?e
112 112 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?e@6
113 113 converting: 5/6 revisions (83.33%)
114 114 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
115 115 scanning paths: /branches/branch\xc3\xa9e 0/1 (0.00%) (esc)
116 116 reparent to file://*/svn-repo (glob)
117 117 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
118 118 reparent to file://*/svn-repo (glob)
119 119 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
120 120 updating tags
121 121 .hgtags
122 122 run hg sink post-conversion action
123 123 $ cd A-hg
124 124 $ hg up
125 125 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126
127 127 Check tags are in UTF-8
128 128
129 129 $ cat .hgtags
130 130 e94e4422020e715add80525e8f0f46c9968689f1 branch\xc3\xa9e (esc)
131 131 f7e66f98380ed1e53a797c5c7a7a2616a7ab377d branch\xc3\xa9 (esc)
132 132
133 133 $ cd ..
@@ -1,247 +1,247 b''
1 1 #require svn svn-bindings
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > convert =
6 6 > EOF
7 7
8 8 $ svnadmin create svn-repo
9 9 $ svnadmin load -q svn-repo < "$TESTDIR/svn/move.svndump"
10 10 $ SVNREPOPATH=`pwd`/svn-repo
11 11 #if windows
12 12 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
13 13 #else
14 14 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
15 15 #endif
16 16
17 17 Convert trunk and branches
18 18
19 19 $ hg convert --datesort "$SVNREPOURL"/subproject A-hg
20 20 initializing destination A-hg repository
21 21 scanning source...
22 22 sorting...
23 23 converting...
24 24 13 createtrunk
25 25 12 moved1
26 26 11 moved1
27 27 10 moved2
28 28 9 changeb and rm d2
29 29 8 changeb and rm d2
30 30 7 moved1again
31 31 6 moved1again
32 32 5 copyfilefrompast
33 33 4 copydirfrompast
34 34 3 add d3
35 35 2 copy dir and remove subdir
36 36 1 add d4old
37 37 0 rename d4old into d4new
38 38
39 39 $ cd A-hg
40 40 $ hg log -G --template '{rev} {desc|firstline} files: {files}\n'
41 41 o 13 rename d4old into d4new files: d4new/g d4old/g
42 42 |
43 43 o 12 add d4old files: d4old/g
44 44 |
45 45 o 11 copy dir and remove subdir files: d3/d31/e d4/d31/e d4/f
46 46 |
47 47 o 10 add d3 files: d3/d31/e d3/f
48 48 |
49 49 o 9 copydirfrompast files: d2/d
50 50 |
51 51 o 8 copyfilefrompast files: d
52 52 |
53 53 o 7 moved1again files: d1/b d1/c
54 54 |
55 55 | o 6 moved1again files:
56 56 | |
57 57 o | 5 changeb and rm d2 files: d1/b d2/d
58 58 | |
59 59 | o 4 changeb and rm d2 files: b
60 60 | |
61 61 o | 3 moved2 files: d2/d
62 62 | |
63 63 o | 2 moved1 files: d1/b d1/c
64 64 | |
65 65 | o 1 moved1 files: b c
66 66 |
67 67 o 0 createtrunk files:
68 68
69 69
70 70 Check move copy records
71 71
72 72 $ hg st --rev 12:13 --copies
73 73 A d4new/g
74 74 d4old/g
75 75 R d4old/g
76 76
77 77 Check branches
78 78
79 79 $ hg branches
80 80 default 13:* (glob)
81 81 d1 6:* (glob)
82 82 $ cd ..
83 83
84 84 $ mkdir test-replace
85 85 $ cd test-replace
86 86 $ svnadmin create svn-repo
87 87 $ svnadmin load -q svn-repo < "$TESTDIR/svn/replace.svndump"
88 88
89 89 Convert files being replaced by directories
90 90
91 91 $ hg convert svn-repo hg-repo
92 92 initializing destination hg-repo repository
93 93 scanning source...
94 94 sorting...
95 95 converting...
96 96 6 initial
97 97 5 clobber symlink
98 98 4 clobber1
99 99 3 clobber2
100 100 2 adddb
101 101 1 clobberdir
102 102 0 branch
103 103
104 104 $ cd hg-repo
105 105
106 106 Manifest before
107 107
108 108 $ hg -v manifest -r 1
109 109 644 a
110 110 644 d/b
111 111 644 d2/a
112 112 644 @ dlink
113 113 644 @ dlink2
114 114 644 dlink3
115 115
116 116 Manifest after clobber1
117 117
118 118 $ hg -v manifest -r 2
119 119 644 a/b
120 120 644 d/b
121 121 644 d2/a
122 122 644 dlink/b
123 123 644 @ dlink2
124 124 644 dlink3
125 125
126 126 Manifest after clobber2
127 127
128 128 $ hg -v manifest -r 3
129 129 644 a/b
130 130 644 d/b
131 131 644 d2/a
132 132 644 dlink/b
133 133 644 @ dlink2
134 134 644 @ dlink3
135 135
136 136 Manifest after clobberdir
137 137
138 138 $ hg -v manifest -r 6
139 139 644 a/b
140 140 644 d/b
141 141 644 d2/a
142 142 644 d2/c
143 143 644 dlink/b
144 144 644 @ dlink2
145 145 644 @ dlink3
146 146
147 147 Try updating
148 148
149 149 $ hg up -qC default
150 150 $ cd ..
151 151
152 152 Test convert progress bar
153 153
154 154 $ cat >> $HGRCPATH <<EOF
155 155 > [extensions]
156 156 > progress =
157 157 > [progress]
158 158 > assume-tty = 1
159 159 > delay = 0
160 160 > changedelay = 0
161 161 > format = topic bar number
162 162 > refresh = 0
163 163 > width = 60
164 164 > EOF
165 165
166 166 $ hg convert svn-repo hg-progress
167 167 \r (no-eol) (esc)
168 scanning [ <=> ] 1\r (no-eol) (esc)
169 scanning [ <=> ] 2\r (no-eol) (esc)
170 scanning [ <=> ] 3\r (no-eol) (esc)
171 scanning [ <=> ] 4\r (no-eol) (esc)
172 scanning [ <=> ] 5\r (no-eol) (esc)
173 scanning [ <=> ] 6\r (no-eol) (esc)
174 scanning [ <=> ] 7\r (no-eol) (esc)
168 scanning [=====> ] 1/7\r (no-eol) (esc)
169 scanning [===========> ] 2/7\r (no-eol) (esc)
170 scanning [=================> ] 3/7\r (no-eol) (esc)
171 scanning [========================> ] 4/7\r (no-eol) (esc)
172 scanning [==============================> ] 5/7\r (no-eol) (esc)
173 scanning [====================================> ] 6/7\r (no-eol) (esc)
174 scanning [===========================================>] 7/7\r (no-eol) (esc)
175 175 \r (no-eol) (esc)
176 176 \r (no-eol) (esc)
177 177 converting [ ] 0/7\r (no-eol) (esc)
178 178 getting files [=====> ] 1/6\r (no-eol) (esc)
179 179 getting files [============> ] 2/6\r (no-eol) (esc)
180 180 getting files [==================> ] 3/6\r (no-eol) (esc)
181 181 getting files [=========================> ] 4/6\r (no-eol) (esc)
182 182 getting files [===============================> ] 5/6\r (no-eol) (esc)
183 183 getting files [======================================>] 6/6\r (no-eol) (esc)
184 184 \r (no-eol) (esc)
185 185 \r (no-eol) (esc)
186 186 converting [=====> ] 1/7\r (no-eol) (esc)
187 187 scanning paths [ ] 0/1\r (no-eol) (esc)
188 188 getting files [======================================>] 1/1\r (no-eol) (esc)
189 189 \r (no-eol) (esc)
190 190 \r (no-eol) (esc)
191 191 converting [===========> ] 2/7\r (no-eol) (esc)
192 192 scanning paths [ ] 0/2\r (no-eol) (esc)
193 193 scanning paths [==================> ] 1/2\r (no-eol) (esc)
194 194 getting files [========> ] 1/4\r (no-eol) (esc)
195 195 getting files [==================> ] 2/4\r (no-eol) (esc)
196 196 getting files [============================> ] 3/4\r (no-eol) (esc)
197 197 getting files [======================================>] 4/4\r (no-eol) (esc)
198 198 \r (no-eol) (esc)
199 199 \r (no-eol) (esc)
200 200 converting [=================> ] 3/7\r (no-eol) (esc)
201 201 scanning paths [ ] 0/1\r (no-eol) (esc)
202 202 getting files [======================================>] 1/1\r (no-eol) (esc)
203 203 \r (no-eol) (esc)
204 204 \r (no-eol) (esc)
205 205 converting [=======================> ] 4/7\r (no-eol) (esc)
206 206 scanning paths [ ] 0/1\r (no-eol) (esc)
207 207 getting files [======================================>] 1/1\r (no-eol) (esc)
208 208 \r (no-eol) (esc)
209 209 \r (no-eol) (esc)
210 210 converting [=============================> ] 5/7\r (no-eol) (esc)
211 211 scanning paths [ ] 0/1\r (no-eol) (esc)
212 212 getting files [===> ] 1/8\r (no-eol) (esc)
213 213 getting files [========> ] 2/8\r (no-eol) (esc)
214 214 getting files [=============> ] 3/8\r (no-eol) (esc)
215 215 getting files [==================> ] 4/8\r (no-eol) (esc)
216 216 getting files [=======================> ] 5/8\r (no-eol) (esc)
217 217 getting files [============================> ] 6/8\r (no-eol) (esc)
218 218 getting files [=================================> ] 7/8\r (no-eol) (esc)
219 219 getting files [======================================>] 8/8\r (no-eol) (esc)
220 220 \r (no-eol) (esc)
221 221 \r (no-eol) (esc)
222 222 converting [===================================> ] 6/7\r (no-eol) (esc)
223 223 scanning paths [ ] 0/3\r (no-eol) (esc)
224 224 scanning paths [===========> ] 1/3\r (no-eol) (esc)
225 225 scanning paths [========================> ] 2/3\r (no-eol) (esc)
226 226 getting files [===> ] 1/8\r (no-eol) (esc)
227 227 getting files [========> ] 2/8\r (no-eol) (esc)
228 228 getting files [=============> ] 3/8\r (no-eol) (esc)
229 229 getting files [==================> ] 4/8\r (no-eol) (esc)
230 230 getting files [=======================> ] 5/8\r (no-eol) (esc)
231 231 getting files [============================> ] 6/8\r (no-eol) (esc)
232 232 getting files [=================================> ] 7/8\r (no-eol) (esc)
233 233 getting files [======================================>] 8/8\r (no-eol) (esc)
234 234 \r (no-eol) (esc)
235 235 initializing destination hg-progress repository
236 236 scanning source...
237 237 sorting...
238 238 converting...
239 239 6 initial
240 240 5 clobber symlink
241 241 4 clobber1
242 242 3 clobber2
243 243 2 adddb
244 244 1 clobberdir
245 245 0 branch
246 246
247 247 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now