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