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