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