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