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