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