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