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