##// END OF EJS Templates
py3: add b'' prefixes in hgext/convert/subversion.py...
Pulkit Goyal -
r38079:c560a4ea default
parent child Browse files
Show More
@@ -1,1361 +1,1361 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 os
7 7 import re
8 8 import tempfile
9 9 import xml.dom.minidom
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import (
13 13 encoding,
14 14 error,
15 15 pycompat,
16 16 util,
17 17 vfs as vfsmod,
18 18 )
19 19 from mercurial.utils import (
20 20 dateutil,
21 21 procutil,
22 22 stringutil,
23 23 )
24 24
25 25 from . import common
26 26
27 27 pickle = util.pickle
28 28 stringio = util.stringio
29 29 propertycache = util.propertycache
30 30 urlerr = util.urlerr
31 31 urlreq = util.urlreq
32 32
33 33 commandline = common.commandline
34 34 commit = common.commit
35 35 converter_sink = common.converter_sink
36 36 converter_source = common.converter_source
37 37 decodeargs = common.decodeargs
38 38 encodeargs = common.encodeargs
39 39 makedatetimestamp = common.makedatetimestamp
40 40 mapfile = common.mapfile
41 41 MissingTool = common.MissingTool
42 42 NoRepo = common.NoRepo
43 43
44 44 # Subversion stuff. Works best with very recent Python SVN bindings
45 45 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
46 46 # these bindings.
47 47
48 48 try:
49 49 import svn
50 50 import svn.client
51 51 import svn.core
52 52 import svn.ra
53 53 import svn.delta
54 54 from . import transport
55 55 import warnings
56 56 warnings.filterwarnings('ignore',
57 57 module='svn.core',
58 58 category=DeprecationWarning)
59 59 svn.core.SubversionException # trigger import to catch error
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(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
70 70 ... b'/proj%20B/mytrunk/mytrunk@1')
71 71 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
72 72 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
73 73 ('', '', 1)
74 74 >>> revsplit(b'@7')
75 75 ('', '', 7)
76 76 >>> revsplit(b'7')
77 77 ('', '', 0)
78 78 >>> revsplit(b'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 urlreq.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 svn.core.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 pycompat.iswindows:
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(stringutil.forcebytestr(inst), fp, protocol)
155 155 else:
156 156 pickle.dump(None, fp, protocol)
157 157 fp.flush()
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 args = decodeargs(ui.fin.read())
172 172 get_log_child(ui.fout, *args)
173 173
174 174 class logstream(object):
175 175 """Interruptible revision log iterator."""
176 176 def __init__(self, stdout):
177 177 self._stdout = stdout
178 178
179 179 def __iter__(self):
180 180 while True:
181 181 try:
182 182 entry = pickle.load(self._stdout)
183 183 except EOFError:
184 184 raise error.Abort(_('Mercurial failed to run itself, check'
185 185 ' hg executable is in PATH'))
186 186 try:
187 187 orig_paths, revnum, author, date, message = entry
188 188 except (TypeError, ValueError):
189 189 if entry is None:
190 190 break
191 191 raise error.Abort(_("log stream exception '%s'") % entry)
192 192 yield entry
193 193
194 194 def close(self):
195 195 if self._stdout:
196 196 self._stdout.close()
197 197 self._stdout = None
198 198
199 199 class directlogstream(list):
200 200 """Direct revision log iterator.
201 201 This can be used for debugging and development but it will probably leak
202 202 memory and is not suitable for real conversions."""
203 203 def __init__(self, url, paths, start, end, limit=0,
204 204 discover_changed_paths=True, strict_node_history=False):
205 205
206 206 def receiver(orig_paths, revnum, author, date, message, pool):
207 207 paths = {}
208 208 if orig_paths is not None:
209 209 for k, v in orig_paths.iteritems():
210 210 paths[k] = changedpath(v)
211 211 self.append((paths, revnum, author, date, message))
212 212
213 213 # Use an ra of our own so that our parent can consume
214 214 # our results without confusing the server.
215 215 t = transport.SvnRaTransport(url=url)
216 216 svn.ra.get_log(t.ra, paths, start, end, limit,
217 217 discover_changed_paths,
218 218 strict_node_history,
219 219 receiver)
220 220
221 221 def close(self):
222 222 pass
223 223
224 224 # Check to see if the given path is a local Subversion repo. Verify this by
225 225 # looking for several svn-specific files and directories in the given
226 226 # directory.
227 227 def filecheck(ui, path, proto):
228 228 for x in ('locks', 'hooks', 'format', 'db'):
229 229 if not os.path.exists(os.path.join(path, x)):
230 230 return False
231 231 return True
232 232
233 233 # Check to see if a given path is the root of an svn repo over http. We verify
234 234 # this by requesting a version-controlled URL we know can't exist and looking
235 235 # for the svn-specific "not found" XML.
236 236 def httpcheck(ui, path, proto):
237 237 try:
238 238 opener = urlreq.buildopener()
239 239 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path), 'rb')
240 240 data = rsp.read()
241 241 except urlerr.httperror as inst:
242 242 if inst.code != 404:
243 243 # Except for 404 we cannot know for sure this is not an svn repo
244 244 ui.warn(_('svn: cannot probe remote repository, assume it could '
245 245 'be a subversion repository. Use --source-type if you '
246 246 'know better.\n'))
247 247 return True
248 248 data = inst.fp.read()
249 249 except Exception:
250 250 # Could be urlerr.urlerror if the URL is invalid or anything else.
251 251 return False
252 252 return '<m:human-readable errcode="160013">' in data
253 253
254 254 protomap = {'http': httpcheck,
255 255 'https': httpcheck,
256 256 'file': filecheck,
257 257 }
258 258 def issvnurl(ui, url):
259 259 try:
260 260 proto, path = url.split('://', 1)
261 261 if proto == 'file':
262 262 if (pycompat.iswindows and path[:1] == '/'
263 263 and path[1:2].isalpha() and path[2:6].lower() == '%3a/'):
264 264 path = path[:2] + ':/' + path[6:]
265 265 path = urlreq.url2pathname(path)
266 266 except ValueError:
267 267 proto = 'file'
268 268 path = os.path.abspath(url)
269 269 if proto == 'file':
270 270 path = util.pconvert(path)
271 271 check = protomap.get(proto, lambda *args: False)
272 272 while '/' in path:
273 273 if check(ui, path, proto):
274 274 return True
275 275 path = path.rsplit('/', 1)[0]
276 276 return False
277 277
278 278 # SVN conversion code stolen from bzr-svn and tailor
279 279 #
280 280 # Subversion looks like a versioned filesystem, branches structures
281 281 # are defined by conventions and not enforced by the tool. First,
282 282 # we define the potential branches (modules) as "trunk" and "branches"
283 283 # children directories. Revisions are then identified by their
284 284 # module and revision number (and a repository identifier).
285 285 #
286 286 # The revision graph is really a tree (or a forest). By default, a
287 287 # revision parent is the previous revision in the same module. If the
288 288 # module directory is copied/moved from another module then the
289 289 # revision is the module root and its parent the source revision in
290 290 # the parent module. A revision has at most one parent.
291 291 #
292 292 class svn_source(converter_source):
293 293 def __init__(self, ui, repotype, url, revs=None):
294 294 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
295 295
296 296 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
297 297 (os.path.exists(url) and
298 298 os.path.exists(os.path.join(url, '.svn'))) or
299 299 issvnurl(ui, url)):
300 300 raise NoRepo(_("%s does not look like a Subversion repository")
301 301 % url)
302 302 if svn is None:
303 303 raise MissingTool(_('could not load Subversion python bindings'))
304 304
305 305 try:
306 306 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
307 307 if version < (1, 4):
308 308 raise MissingTool(_('Subversion python bindings %d.%d found, '
309 309 '1.4 or later required') % version)
310 310 except AttributeError:
311 311 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
312 312 'or later required'))
313 313
314 314 self.lastrevs = {}
315 315
316 316 latest = None
317 317 try:
318 318 # Support file://path@rev syntax. Useful e.g. to convert
319 319 # deleted branches.
320 320 at = url.rfind('@')
321 321 if at >= 0:
322 322 latest = int(url[at + 1:])
323 323 url = url[:at]
324 324 except ValueError:
325 325 pass
326 326 self.url = geturl(url)
327 327 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
328 328 try:
329 329 self.transport = transport.SvnRaTransport(url=self.url)
330 330 self.ra = self.transport.ra
331 331 self.ctx = self.transport.client
332 332 self.baseurl = svn.ra.get_repos_root(self.ra)
333 333 # Module is either empty or a repository path starting with
334 334 # a slash and not ending with a slash.
335 335 self.module = urlreq.unquote(self.url[len(self.baseurl):])
336 336 self.prevmodule = None
337 337 self.rootmodule = self.module
338 338 self.commits = {}
339 339 self.paths = {}
340 340 self.uuid = svn.ra.get_uuid(self.ra)
341 341 except svn.core.SubversionException:
342 342 ui.traceback()
343 343 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
344 344 svn.core.SVN_VER_MINOR,
345 345 svn.core.SVN_VER_MICRO)
346 346 raise NoRepo(_("%s does not look like a Subversion repository "
347 347 "to libsvn version %s")
348 348 % (self.url, svnversion))
349 349
350 350 if revs:
351 351 if len(revs) > 1:
352 352 raise error.Abort(_('subversion source does not support '
353 353 'specifying multiple revisions'))
354 354 try:
355 355 latest = int(revs[0])
356 356 except ValueError:
357 357 raise error.Abort(_('svn: revision %s is not an integer') %
358 358 revs[0])
359 359
360 360 trunkcfg = self.ui.config('convert', 'svn.trunk')
361 361 if trunkcfg is None:
362 362 trunkcfg = 'trunk'
363 363 self.trunkname = trunkcfg.strip('/')
364 364 self.startrev = self.ui.config('convert', 'svn.startrev')
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:
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 svn.core.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 r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
539 539 r'{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 'ab')
648 648 self.convertfp.write(util.tonativeeol('%s %d\n'
649 649 % (destrev, self.revnum(rev))))
650 650 self.convertfp.flush()
651 651
652 652 def revid(self, revnum, module=None):
653 653 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
654 654
655 655 def revnum(self, rev):
656 656 return int(rev.split('@')[-1])
657 657
658 658 def latest(self, path, stop=None):
659 659 """Find the latest revid affecting path, up to stop revision
660 660 number. If stop is None, default to repository latest
661 661 revision. It may return a revision in a different module,
662 662 since a branch may be moved without a change being
663 663 reported. Return None if computed module does not belong to
664 664 rootmodule subtree.
665 665 """
666 666 def findchanges(path, start, stop=None):
667 667 stream = self._getlog([path], start, stop or 1)
668 668 try:
669 669 for entry in stream:
670 670 paths, revnum, author, date, message = entry
671 671 if stop is None and paths:
672 672 # We do not know the latest changed revision,
673 673 # keep the first one with changed paths.
674 674 break
675 675 if revnum <= stop:
676 676 break
677 677
678 678 for p in paths:
679 679 if (not path.startswith(p) or
680 680 not paths[p].copyfrom_path):
681 681 continue
682 682 newpath = paths[p].copyfrom_path + path[len(p):]
683 683 self.ui.debug("branch renamed from %s to %s at %d\n" %
684 684 (path, newpath, revnum))
685 685 path = newpath
686 686 break
687 687 if not paths:
688 688 revnum = None
689 689 return revnum, path
690 690 finally:
691 691 stream.close()
692 692
693 693 if not path.startswith(self.rootmodule):
694 694 # Requests on foreign branches may be forbidden at server level
695 695 self.ui.debug('ignoring foreign branch %r\n' % path)
696 696 return None
697 697
698 698 if stop is None:
699 699 stop = svn.ra.get_latest_revnum(self.ra)
700 700 try:
701 701 prevmodule = self.reparent('')
702 702 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
703 703 self.reparent(prevmodule)
704 704 except svn.core.SubversionException:
705 705 dirent = None
706 706 if not dirent:
707 707 raise SvnPathNotFound(_('%s not found up to revision %d')
708 708 % (path, stop))
709 709
710 710 # stat() gives us the previous revision on this line of
711 711 # development, but it might be in *another module*. Fetch the
712 712 # log and detect renames down to the latest revision.
713 713 revnum, realpath = findchanges(path, stop, dirent.created_rev)
714 714 if revnum is None:
715 715 # Tools like svnsync can create empty revision, when
716 716 # synchronizing only a subtree for instance. These empty
717 717 # revisions created_rev still have their original values
718 718 # despite all changes having disappeared and can be
719 719 # returned by ra.stat(), at least when stating the root
720 720 # module. In that case, do not trust created_rev and scan
721 721 # the whole history.
722 722 revnum, realpath = findchanges(path, stop)
723 723 if revnum is None:
724 724 self.ui.debug('ignoring empty branch %r\n' % realpath)
725 725 return None
726 726
727 727 if not realpath.startswith(self.rootmodule):
728 728 self.ui.debug('ignoring foreign branch %r\n' % realpath)
729 729 return None
730 730 return self.revid(revnum, realpath)
731 731
732 732 def reparent(self, module):
733 733 """Reparent the svn transport and return the previous parent."""
734 734 if self.prevmodule == module:
735 735 return module
736 736 svnurl = self.baseurl + quote(module)
737 737 prevmodule = self.prevmodule
738 738 if prevmodule is None:
739 739 prevmodule = ''
740 740 self.ui.debug("reparent to %s\n" % svnurl)
741 741 svn.ra.reparent(self.ra, svnurl)
742 742 self.prevmodule = module
743 743 return prevmodule
744 744
745 745 def expandpaths(self, rev, paths, parents):
746 746 changed, removed = set(), set()
747 747 copies = {}
748 748
749 749 new_module, revnum = revsplit(rev)[1:]
750 750 if new_module != self.module:
751 751 self.module = new_module
752 752 self.reparent(self.module)
753 753
754 754 for i, (path, ent) in enumerate(paths):
755 755 self.ui.progress(_('scanning paths'), i, item=path,
756 756 total=len(paths), unit=_('paths'))
757 757 entrypath = self.getrelpath(path)
758 758
759 759 kind = self._checkpath(entrypath, revnum)
760 760 if kind == svn.core.svn_node_file:
761 761 changed.add(self.recode(entrypath))
762 762 if not ent.copyfrom_path or not parents:
763 763 continue
764 764 # Copy sources not in parent revisions cannot be
765 765 # represented, ignore their origin for now
766 766 pmodule, prevnum = revsplit(parents[0])[1:]
767 767 if ent.copyfrom_rev < prevnum:
768 768 continue
769 769 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
770 770 if not copyfrom_path:
771 771 continue
772 772 self.ui.debug("copied to %s from %s@%s\n" %
773 773 (entrypath, copyfrom_path, ent.copyfrom_rev))
774 774 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
775 775 elif kind == 0: # gone, but had better be a deleted *file*
776 776 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
777 777 pmodule, prevnum = revsplit(parents[0])[1:]
778 778 parentpath = pmodule + "/" + entrypath
779 779 fromkind = self._checkpath(entrypath, prevnum, pmodule)
780 780
781 781 if fromkind == svn.core.svn_node_file:
782 782 removed.add(self.recode(entrypath))
783 783 elif fromkind == svn.core.svn_node_dir:
784 784 oroot = parentpath.strip('/')
785 785 nroot = path.strip('/')
786 786 children = self._iterfiles(oroot, prevnum)
787 787 for childpath in children:
788 788 childpath = childpath.replace(oroot, nroot)
789 789 childpath = self.getrelpath("/" + childpath, pmodule)
790 790 if childpath:
791 791 removed.add(self.recode(childpath))
792 792 else:
793 793 self.ui.debug('unknown path in revision %d: %s\n' % \
794 794 (revnum, path))
795 795 elif kind == svn.core.svn_node_dir:
796 796 if ent.action == 'M':
797 797 # If the directory just had a prop change,
798 798 # then we shouldn't need to look for its children.
799 799 continue
800 800 if ent.action == 'R' and parents:
801 801 # If a directory is replacing a file, mark the previous
802 802 # file as deleted
803 803 pmodule, prevnum = revsplit(parents[0])[1:]
804 804 pkind = self._checkpath(entrypath, prevnum, pmodule)
805 805 if pkind == svn.core.svn_node_file:
806 806 removed.add(self.recode(entrypath))
807 807 elif pkind == svn.core.svn_node_dir:
808 808 # We do not know what files were kept or removed,
809 809 # mark them all as changed.
810 810 for childpath in self._iterfiles(pmodule, prevnum):
811 811 childpath = self.getrelpath("/" + childpath)
812 812 if childpath:
813 813 changed.add(self.recode(childpath))
814 814
815 815 for childpath in self._iterfiles(path, revnum):
816 816 childpath = self.getrelpath("/" + childpath)
817 817 if childpath:
818 818 changed.add(self.recode(childpath))
819 819
820 820 # Handle directory copies
821 821 if not ent.copyfrom_path or not parents:
822 822 continue
823 823 # Copy sources not in parent revisions cannot be
824 824 # represented, ignore their origin for now
825 825 pmodule, prevnum = revsplit(parents[0])[1:]
826 826 if ent.copyfrom_rev < prevnum:
827 827 continue
828 828 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
829 829 if not copyfrompath:
830 830 continue
831 831 self.ui.debug("mark %s came from %s:%d\n"
832 832 % (path, copyfrompath, ent.copyfrom_rev))
833 833 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
834 834 for childpath in children:
835 835 childpath = self.getrelpath("/" + childpath, pmodule)
836 836 if not childpath:
837 837 continue
838 838 copytopath = path + childpath[len(copyfrompath):]
839 839 copytopath = self.getrelpath(copytopath)
840 840 copies[self.recode(copytopath)] = self.recode(childpath)
841 841
842 842 self.ui.progress(_('scanning paths'), None)
843 843 changed.update(removed)
844 844 return (list(changed), removed, copies)
845 845
846 846 def _fetch_revisions(self, from_revnum, to_revnum):
847 847 if from_revnum < to_revnum:
848 848 from_revnum, to_revnum = to_revnum, from_revnum
849 849
850 850 self.child_cset = None
851 851
852 852 def parselogentry(orig_paths, revnum, author, date, message):
853 853 """Return the parsed commit object or None, and True if
854 854 the revision is a branch root.
855 855 """
856 856 self.ui.debug("parsing revision %d (%d changes)\n" %
857 857 (revnum, len(orig_paths)))
858 858
859 859 branched = False
860 860 rev = self.revid(revnum)
861 861 # branch log might return entries for a parent we already have
862 862
863 863 if rev in self.commits or revnum < to_revnum:
864 864 return None, branched
865 865
866 866 parents = []
867 867 # check whether this revision is the start of a branch or part
868 868 # of a branch renaming
869 869 orig_paths = sorted(orig_paths.iteritems())
870 870 root_paths = [(p, e) for p, e in orig_paths
871 871 if self.module.startswith(p)]
872 872 if root_paths:
873 873 path, ent = root_paths[-1]
874 874 if ent.copyfrom_path:
875 875 branched = True
876 876 newpath = ent.copyfrom_path + self.module[len(path):]
877 877 # ent.copyfrom_rev may not be the actual last revision
878 878 previd = self.latest(newpath, ent.copyfrom_rev)
879 879 if previd is not None:
880 880 prevmodule, prevnum = revsplit(previd)[1:]
881 881 if prevnum >= self.startrev:
882 882 parents = [previd]
883 883 self.ui.note(
884 884 _('found parent of branch %s at %d: %s\n') %
885 885 (self.module, prevnum, prevmodule))
886 886 else:
887 887 self.ui.debug("no copyfrom path, don't know what to do.\n")
888 888
889 889 paths = []
890 890 # filter out unrelated paths
891 891 for path, ent in orig_paths:
892 892 if self.getrelpath(path) is None:
893 893 continue
894 894 paths.append((path, ent))
895 895
896 896 # Example SVN datetime. Includes microseconds.
897 897 # ISO-8601 conformant
898 898 # '2007-01-04T17:35:00.902377Z'
899 899 date = dateutil.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
900 900 if self.ui.configbool('convert', 'localtimezone'):
901 901 date = makedatetimestamp(date[0])
902 902
903 903 if message:
904 904 log = self.recode(message)
905 905 else:
906 906 log = ''
907 907
908 908 if author:
909 909 author = self.recode(author)
910 910 else:
911 911 author = ''
912 912
913 913 try:
914 914 branch = self.module.split("/")[-1]
915 915 if branch == self.trunkname:
916 916 branch = None
917 917 except IndexError:
918 918 branch = None
919 919
920 920 cset = commit(author=author,
921 921 date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
922 922 desc=log,
923 923 parents=parents,
924 924 branch=branch,
925 925 rev=rev)
926 926
927 927 self.commits[rev] = cset
928 928 # The parents list is *shared* among self.paths and the
929 929 # commit object. Both will be updated below.
930 930 self.paths[rev] = (paths, cset.parents)
931 931 if self.child_cset and not self.child_cset.parents:
932 932 self.child_cset.parents[:] = [rev]
933 933 self.child_cset = cset
934 934 return cset, branched
935 935
936 936 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
937 937 (self.module, from_revnum, to_revnum))
938 938
939 939 try:
940 940 firstcset = None
941 941 lastonbranch = False
942 942 stream = self._getlog([self.module], from_revnum, to_revnum)
943 943 try:
944 944 for entry in stream:
945 945 paths, revnum, author, date, message = entry
946 946 if revnum < self.startrev:
947 947 lastonbranch = True
948 948 break
949 949 if not paths:
950 950 self.ui.debug('revision %d has no entries\n' % revnum)
951 951 # If we ever leave the loop on an empty
952 952 # revision, do not try to get a parent branch
953 953 lastonbranch = lastonbranch or revnum == 0
954 954 continue
955 955 cset, lastonbranch = parselogentry(paths, revnum, author,
956 956 date, message)
957 957 if cset:
958 958 firstcset = cset
959 959 if lastonbranch:
960 960 break
961 961 finally:
962 962 stream.close()
963 963
964 964 if not lastonbranch and firstcset and not firstcset.parents:
965 965 # The first revision of the sequence (the last fetched one)
966 966 # has invalid parents if not a branch root. Find the parent
967 967 # revision now, if any.
968 968 try:
969 969 firstrevnum = self.revnum(firstcset.rev)
970 970 if firstrevnum > 1:
971 971 latest = self.latest(self.module, firstrevnum - 1)
972 972 if latest:
973 973 firstcset.parents.append(latest)
974 974 except SvnPathNotFound:
975 975 pass
976 976 except svn.core.SubversionException as xxx_todo_changeme:
977 977 (inst, num) = xxx_todo_changeme.args
978 978 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
979 979 raise error.Abort(_('svn: branch has no revision %s')
980 980 % to_revnum)
981 981 raise
982 982
983 983 def getfile(self, file, rev):
984 984 # TODO: ra.get_file transmits the whole file instead of diffs.
985 985 if file in self.removed:
986 986 return None, None
987 987 mode = ''
988 988 try:
989 989 new_module, revnum = revsplit(rev)[1:]
990 990 if self.module != new_module:
991 991 self.module = new_module
992 992 self.reparent(self.module)
993 993 io = stringio()
994 994 info = svn.ra.get_file(self.ra, file, revnum, io)
995 995 data = io.getvalue()
996 996 # ra.get_file() seems to keep a reference on the input buffer
997 997 # preventing collection. Release it explicitly.
998 998 io.close()
999 999 if isinstance(info, list):
1000 1000 info = info[-1]
1001 1001 mode = ("svn:executable" in info) and 'x' or ''
1002 1002 mode = ("svn:special" in info) and 'l' or mode
1003 1003 except svn.core.SubversionException as e:
1004 1004 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1005 1005 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1006 1006 if e.apr_err in notfound: # File not found
1007 1007 return None, None
1008 1008 raise
1009 1009 if mode == 'l':
1010 1010 link_prefix = "link "
1011 1011 if data.startswith(link_prefix):
1012 1012 data = data[len(link_prefix):]
1013 1013 return data, mode
1014 1014
1015 1015 def _iterfiles(self, path, revnum):
1016 1016 """Enumerate all files in path at revnum, recursively."""
1017 1017 path = path.strip('/')
1018 1018 pool = svn.core.Pool()
1019 1019 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1020 1020 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1021 1021 if path:
1022 1022 path += '/'
1023 1023 return ((path + p) for p, e in entries.iteritems()
1024 1024 if e.kind == svn.core.svn_node_file)
1025 1025
1026 1026 def getrelpath(self, path, module=None):
1027 1027 if module is None:
1028 1028 module = self.module
1029 1029 # Given the repository url of this wc, say
1030 1030 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1031 1031 # extract the "entry" portion (a relative path) from what
1032 1032 # svn log --xml says, i.e.
1033 1033 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1034 1034 # that is to say "tests/PloneTestCase.py"
1035 1035 if path.startswith(module):
1036 1036 relative = path.rstrip('/')[len(module):]
1037 1037 if relative.startswith('/'):
1038 1038 return relative[1:]
1039 1039 elif relative == '':
1040 1040 return relative
1041 1041
1042 1042 # The path is outside our tracked tree...
1043 1043 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1044 1044 return None
1045 1045
1046 1046 def _checkpath(self, path, revnum, module=None):
1047 1047 if module is not None:
1048 1048 prevmodule = self.reparent('')
1049 1049 path = module + '/' + path
1050 1050 try:
1051 1051 # ra.check_path does not like leading slashes very much, it leads
1052 1052 # to PROPFIND subversion errors
1053 1053 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1054 1054 finally:
1055 1055 if module is not None:
1056 1056 self.reparent(prevmodule)
1057 1057
1058 1058 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1059 1059 strict_node_history=False):
1060 1060 # Normalize path names, svn >= 1.5 only wants paths relative to
1061 1061 # supplied URL
1062 1062 relpaths = []
1063 1063 for p in paths:
1064 1064 if not p.startswith('/'):
1065 1065 p = self.module + '/' + p
1066 1066 relpaths.append(p.strip('/'))
1067 1067 args = [self.baseurl, relpaths, start, end, limit,
1068 1068 discover_changed_paths, strict_node_history]
1069 1069 # developer config: convert.svn.debugsvnlog
1070 1070 if not self.ui.configbool('convert', 'svn.debugsvnlog'):
1071 1071 return directlogstream(*args)
1072 1072 arg = encodeargs(args)
1073 1073 hgexe = procutil.hgexecutable()
1074 1074 cmd = '%s debugsvnlog' % procutil.shellquote(hgexe)
1075 1075 stdin, stdout = procutil.popen2(procutil.quotecommand(cmd))
1076 1076 stdin.write(arg)
1077 1077 try:
1078 1078 stdin.close()
1079 1079 except IOError:
1080 1080 raise error.Abort(_('Mercurial failed to run itself, check'
1081 1081 ' hg executable is in PATH'))
1082 1082 return logstream(stdout)
1083 1083
1084 pre_revprop_change = '''#!/bin/sh
1084 pre_revprop_change = b'''#!/bin/sh
1085 1085
1086 1086 REPOS="$1"
1087 1087 REV="$2"
1088 1088 USER="$3"
1089 1089 PROPNAME="$4"
1090 1090 ACTION="$5"
1091 1091
1092 1092 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1093 1093 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1094 1094 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1095 1095
1096 1096 echo "Changing prohibited revision property" >&2
1097 1097 exit 1
1098 1098 '''
1099 1099
1100 1100 class svn_sink(converter_sink, commandline):
1101 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1102 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1101 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1102 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1103 1103
1104 1104 def prerun(self):
1105 1105 if self.wc:
1106 1106 os.chdir(self.wc)
1107 1107
1108 1108 def postrun(self):
1109 1109 if self.wc:
1110 1110 os.chdir(self.cwd)
1111 1111
1112 1112 def join(self, name):
1113 1113 return os.path.join(self.wc, '.svn', name)
1114 1114
1115 1115 def revmapfile(self):
1116 1116 return self.join('hg-shamap')
1117 1117
1118 1118 def authorfile(self):
1119 1119 return self.join('hg-authormap')
1120 1120
1121 1121 def __init__(self, ui, repotype, path):
1122 1122
1123 1123 converter_sink.__init__(self, ui, repotype, path)
1124 1124 commandline.__init__(self, ui, 'svn')
1125 1125 self.delete = []
1126 1126 self.setexec = []
1127 1127 self.delexec = []
1128 1128 self.copies = []
1129 1129 self.wc = None
1130 1130 self.cwd = pycompat.getcwd()
1131 1131
1132 1132 created = False
1133 1133 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1134 1134 self.wc = os.path.realpath(path)
1135 1135 self.run0('update')
1136 1136 else:
1137 1137 if not re.search(br'^(file|http|https|svn|svn\+ssh)\://', path):
1138 1138 path = os.path.realpath(path)
1139 1139 if os.path.isdir(os.path.dirname(path)):
1140 1140 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1141 1141 ui.status(_('initializing svn repository %r\n') %
1142 1142 os.path.basename(path))
1143 1143 commandline(ui, 'svnadmin').run0('create', path)
1144 1144 created = path
1145 1145 path = util.normpath(path)
1146 1146 if not path.startswith('/'):
1147 1147 path = '/' + path
1148 1148 path = 'file://' + path
1149 1149
1150 1150 wcpath = os.path.join(pycompat.getcwd(), os.path.basename(path) +
1151 1151 '-wc')
1152 1152 ui.status(_('initializing svn working copy %r\n')
1153 1153 % os.path.basename(wcpath))
1154 1154 self.run0('checkout', path, wcpath)
1155 1155
1156 1156 self.wc = wcpath
1157 1157 self.opener = vfsmod.vfs(self.wc)
1158 1158 self.wopener = vfsmod.vfs(self.wc)
1159 1159 self.childmap = mapfile(ui, self.join('hg-childmap'))
1160 1160 if util.checkexec(self.wc):
1161 1161 self.is_exec = util.isexec
1162 1162 else:
1163 1163 self.is_exec = None
1164 1164
1165 1165 if created:
1166 1166 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1167 1167 fp = open(hook, 'wb')
1168 1168 fp.write(pre_revprop_change)
1169 1169 fp.close()
1170 1170 util.setflags(hook, False, True)
1171 1171
1172 1172 output = self.run0('info')
1173 1173 self.uuid = self.uuid_re.search(output).group(1).strip()
1174 1174
1175 1175 def wjoin(self, *names):
1176 1176 return os.path.join(self.wc, *names)
1177 1177
1178 1178 @propertycache
1179 1179 def manifest(self):
1180 1180 # As of svn 1.7, the "add" command fails when receiving
1181 1181 # already tracked entries, so we have to track and filter them
1182 1182 # ourselves.
1183 1183 m = set()
1184 1184 output = self.run0('ls', recursive=True, xml=True)
1185 1185 doc = xml.dom.minidom.parseString(output)
1186 1186 for e in doc.getElementsByTagName('entry'):
1187 1187 for n in e.childNodes:
1188 1188 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1189 1189 continue
1190 1190 name = ''.join(c.data for c in n.childNodes
1191 1191 if c.nodeType == c.TEXT_NODE)
1192 1192 # Entries are compared with names coming from
1193 1193 # mercurial, so bytes with undefined encoding. Our
1194 1194 # best bet is to assume they are in local
1195 1195 # encoding. They will be passed to command line calls
1196 1196 # later anyway, so they better be.
1197 1197 m.add(encoding.unitolocal(name))
1198 1198 break
1199 1199 return m
1200 1200
1201 1201 def putfile(self, filename, flags, data):
1202 1202 if 'l' in flags:
1203 1203 self.wopener.symlink(data, filename)
1204 1204 else:
1205 1205 try:
1206 1206 if os.path.islink(self.wjoin(filename)):
1207 1207 os.unlink(filename)
1208 1208 except OSError:
1209 1209 pass
1210 1210 self.wopener.write(filename, data)
1211 1211
1212 1212 if self.is_exec:
1213 1213 if self.is_exec(self.wjoin(filename)):
1214 1214 if 'x' not in flags:
1215 1215 self.delexec.append(filename)
1216 1216 else:
1217 1217 if 'x' in flags:
1218 1218 self.setexec.append(filename)
1219 1219 util.setflags(self.wjoin(filename), False, 'x' in flags)
1220 1220
1221 1221 def _copyfile(self, source, dest):
1222 1222 # SVN's copy command pukes if the destination file exists, but
1223 1223 # our copyfile method expects to record a copy that has
1224 1224 # already occurred. Cross the semantic gap.
1225 1225 wdest = self.wjoin(dest)
1226 1226 exists = os.path.lexists(wdest)
1227 1227 if exists:
1228 1228 fd, tempname = tempfile.mkstemp(
1229 1229 prefix='hg-copy-', dir=os.path.dirname(wdest))
1230 1230 os.close(fd)
1231 1231 os.unlink(tempname)
1232 1232 os.rename(wdest, tempname)
1233 1233 try:
1234 1234 self.run0('copy', source, dest)
1235 1235 finally:
1236 1236 self.manifest.add(dest)
1237 1237 if exists:
1238 1238 try:
1239 1239 os.unlink(wdest)
1240 1240 except OSError:
1241 1241 pass
1242 1242 os.rename(tempname, wdest)
1243 1243
1244 1244 def dirs_of(self, files):
1245 1245 dirs = set()
1246 1246 for f in files:
1247 1247 if os.path.isdir(self.wjoin(f)):
1248 1248 dirs.add(f)
1249 1249 i = len(f)
1250 1250 for i in iter(lambda: f.rfind('/', 0, i), -1):
1251 1251 dirs.add(f[:i])
1252 1252 return dirs
1253 1253
1254 1254 def add_dirs(self, files):
1255 1255 add_dirs = [d for d in sorted(self.dirs_of(files))
1256 1256 if d not in self.manifest]
1257 1257 if add_dirs:
1258 1258 self.manifest.update(add_dirs)
1259 1259 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1260 1260 return add_dirs
1261 1261
1262 1262 def add_files(self, files):
1263 1263 files = [f for f in files if f not in self.manifest]
1264 1264 if files:
1265 1265 self.manifest.update(files)
1266 1266 self.xargs(files, 'add', quiet=True)
1267 1267 return files
1268 1268
1269 1269 def addchild(self, parent, child):
1270 1270 self.childmap[parent] = child
1271 1271
1272 1272 def revid(self, rev):
1273 1273 return u"svn:%s@%s" % (self.uuid, rev)
1274 1274
1275 1275 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1276 1276 cleanp2):
1277 1277 for parent in parents:
1278 1278 try:
1279 1279 return self.revid(self.childmap[parent])
1280 1280 except KeyError:
1281 1281 pass
1282 1282
1283 1283 # Apply changes to working copy
1284 1284 for f, v in files:
1285 1285 data, mode = source.getfile(f, v)
1286 1286 if data is None:
1287 1287 self.delete.append(f)
1288 1288 else:
1289 1289 self.putfile(f, mode, data)
1290 1290 if f in copies:
1291 1291 self.copies.append([copies[f], f])
1292 1292 if full:
1293 1293 self.delete.extend(sorted(self.manifest.difference(files)))
1294 1294 files = [f[0] for f in files]
1295 1295
1296 1296 entries = set(self.delete)
1297 1297 files = frozenset(files)
1298 1298 entries.update(self.add_dirs(files.difference(entries)))
1299 1299 if self.copies:
1300 1300 for s, d in self.copies:
1301 1301 self._copyfile(s, d)
1302 1302 self.copies = []
1303 1303 if self.delete:
1304 1304 self.xargs(self.delete, 'delete')
1305 1305 for f in self.delete:
1306 1306 self.manifest.remove(f)
1307 1307 self.delete = []
1308 1308 entries.update(self.add_files(files.difference(entries)))
1309 1309 if self.delexec:
1310 1310 self.xargs(self.delexec, 'propdel', 'svn:executable')
1311 1311 self.delexec = []
1312 1312 if self.setexec:
1313 1313 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1314 1314 self.setexec = []
1315 1315
1316 1316 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1317 1317 fp = os.fdopen(fd, r'wb')
1318 1318 fp.write(util.tonativeeol(commit.desc))
1319 1319 fp.close()
1320 1320 try:
1321 1321 output = self.run0('commit',
1322 1322 username=stringutil.shortuser(commit.author),
1323 1323 file=messagefile,
1324 1324 encoding='utf-8')
1325 1325 try:
1326 1326 rev = self.commit_re.search(output).group(1)
1327 1327 except AttributeError:
1328 1328 if parents and not files:
1329 1329 return parents[0]
1330 1330 self.ui.warn(_('unexpected svn output:\n'))
1331 1331 self.ui.warn(output)
1332 1332 raise error.Abort(_('unable to cope with svn output'))
1333 1333 if commit.rev:
1334 1334 self.run('propset', 'hg:convert-rev', commit.rev,
1335 1335 revprop=True, revision=rev)
1336 1336 if commit.branch and commit.branch != 'default':
1337 1337 self.run('propset', 'hg:convert-branch', commit.branch,
1338 1338 revprop=True, revision=rev)
1339 1339 for parent in parents:
1340 1340 self.addchild(parent, rev)
1341 1341 return self.revid(rev)
1342 1342 finally:
1343 1343 os.unlink(messagefile)
1344 1344
1345 1345 def puttags(self, tags):
1346 1346 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1347 1347 return None, None
1348 1348
1349 1349 def hascommitfrommap(self, rev):
1350 1350 # We trust that revisions referenced in a map still is present
1351 1351 # TODO: implement something better if necessary and feasible
1352 1352 return True
1353 1353
1354 1354 def hascommitforsplicemap(self, rev):
1355 1355 # This is not correct as one can convert to an existing subversion
1356 1356 # repository and childmap would not list all revisions. Too bad.
1357 1357 if rev in self.childmap:
1358 1358 return True
1359 1359 raise error.Abort(_('splice map revision %s not found in subversion '
1360 1360 'child map (revision lookups are not implemented)')
1361 1361 % rev)
General Comments 0
You need to be logged in to leave comments. Login now