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