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