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