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