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