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