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