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