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