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