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