##// END OF EJS Templates
convert: svn_sink: workaround of command line size limitation on win32....
Shun-ichi GOTO -
r5790:f85c0034 default
parent child Browse files
Show More
@@ -1,924 +1,949 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 # Configuration options:
6 6 #
7 7 # convert.svn.trunk
8 8 # Relative path to the trunk (default: "trunk")
9 9 # convert.svn.branches
10 10 # Relative path to tree of branches (default: "branches")
11 11 # convert.svn.tags
12 12 # Relative path to tree of tags (default: "tags")
13 13 #
14 14 # Set these in a hgrc, or on the command line as follows:
15 15 #
16 16 # hg convert --config convert.svn.trunk=wackoname [...]
17 17
18 18 import locale
19 19 import os
20 20 import re
21 21 import sys
22 22 import cPickle as pickle
23 23 import tempfile
24 24
25 25 from mercurial import strutil, util
26 26 from mercurial.i18n import _
27 27
28 28 # Subversion stuff. Works best with very recent Python SVN bindings
29 29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 30 # these bindings.
31 31
32 32 from cStringIO import StringIO
33 33
34 34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 35 from common import commandline, converter_sink, mapfile
36 36
37 37 try:
38 38 from svn.core import SubversionException, Pool
39 39 import svn
40 40 import svn.client
41 41 import svn.core
42 42 import svn.ra
43 43 import svn.delta
44 44 import transport
45 45 except ImportError:
46 46 pass
47 47
48 48 def geturl(path):
49 49 try:
50 50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 51 except SubversionException:
52 52 pass
53 53 if os.path.isdir(path):
54 54 return 'file://%s' % os.path.normpath(os.path.abspath(path))
55 55 return path
56 56
57 57 def optrev(number):
58 58 optrev = svn.core.svn_opt_revision_t()
59 59 optrev.kind = svn.core.svn_opt_revision_number
60 60 optrev.value.number = number
61 61 return optrev
62 62
63 63 class changedpath(object):
64 64 def __init__(self, p):
65 65 self.copyfrom_path = p.copyfrom_path
66 66 self.copyfrom_rev = p.copyfrom_rev
67 67 self.action = p.action
68 68
69 69 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
70 70 strict_node_history=False):
71 71 protocol = -1
72 72 def receiver(orig_paths, revnum, author, date, message, pool):
73 73 if orig_paths is not None:
74 74 for k, v in orig_paths.iteritems():
75 75 orig_paths[k] = changedpath(v)
76 76 pickle.dump((orig_paths, revnum, author, date, message),
77 77 fp, protocol)
78 78
79 79 try:
80 80 # Use an ra of our own so that our parent can consume
81 81 # our results without confusing the server.
82 82 t = transport.SvnRaTransport(url=url)
83 83 svn.ra.get_log(t.ra, paths, start, end, limit,
84 84 discover_changed_paths,
85 85 strict_node_history,
86 86 receiver)
87 87 except SubversionException, (inst, num):
88 88 pickle.dump(num, fp, protocol)
89 89 else:
90 90 pickle.dump(None, fp, protocol)
91 91 fp.close()
92 92
93 93 def debugsvnlog(ui, **opts):
94 94 """Fetch SVN log in a subprocess and channel them back to parent to
95 95 avoid memory collection issues.
96 96 """
97 97 util.set_binary(sys.stdin)
98 98 util.set_binary(sys.stdout)
99 99 args = decodeargs(sys.stdin.read())
100 100 get_log_child(sys.stdout, *args)
101 101
102 102 # SVN conversion code stolen from bzr-svn and tailor
103 103 class svn_source(converter_source):
104 104 def __init__(self, ui, url, rev=None):
105 105 super(svn_source, self).__init__(ui, url, rev=rev)
106 106
107 107 try:
108 108 SubversionException
109 109 except NameError:
110 110 raise NoRepo('Subversion python bindings could not be loaded')
111 111
112 112 self.encoding = locale.getpreferredencoding()
113 113 self.lastrevs = {}
114 114
115 115 latest = None
116 116 try:
117 117 # Support file://path@rev syntax. Useful e.g. to convert
118 118 # deleted branches.
119 119 at = url.rfind('@')
120 120 if at >= 0:
121 121 latest = int(url[at+1:])
122 122 url = url[:at]
123 123 except ValueError, e:
124 124 pass
125 125 self.url = geturl(url)
126 126 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
127 127 try:
128 128 self.transport = transport.SvnRaTransport(url=self.url)
129 129 self.ra = self.transport.ra
130 130 self.ctx = self.transport.client
131 131 self.base = svn.ra.get_repos_root(self.ra)
132 132 self.module = self.url[len(self.base):]
133 133 self.modulemap = {} # revision, module
134 134 self.commits = {}
135 135 self.paths = {}
136 136 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
137 137 except SubversionException, e:
138 138 ui.print_exc()
139 139 raise NoRepo("%s does not look like a Subversion repo" % self.url)
140 140
141 141 if rev:
142 142 try:
143 143 latest = int(rev)
144 144 except ValueError:
145 145 raise util.Abort('svn: revision %s is not an integer' % rev)
146 146
147 147 try:
148 148 self.get_blacklist()
149 149 except IOError, e:
150 150 pass
151 151
152 152 self.last_changed = self.latest(self.module, latest)
153 153
154 154 self.head = self.revid(self.last_changed)
155 155 self._changescache = None
156 156
157 157 if os.path.exists(os.path.join(url, '.svn/entries')):
158 158 self.wc = url
159 159 else:
160 160 self.wc = None
161 161 self.convertfp = None
162 162
163 163 def setrevmap(self, revmap):
164 164 lastrevs = {}
165 165 for revid in revmap.iterkeys():
166 166 uuid, module, revnum = self.revsplit(revid)
167 167 lastrevnum = lastrevs.setdefault(module, revnum)
168 168 if revnum > lastrevnum:
169 169 lastrevs[module] = revnum
170 170 self.lastrevs = lastrevs
171 171
172 172 def exists(self, path, optrev):
173 173 try:
174 174 svn.client.ls(self.url.rstrip('/') + '/' + path,
175 175 optrev, False, self.ctx)
176 176 return True
177 177 except SubversionException, err:
178 178 return False
179 179
180 180 def getheads(self):
181 181 # detect standard /branches, /tags, /trunk layout
182 182 rev = optrev(self.last_changed)
183 183 rpath = self.url.strip('/')
184 184 cfgtrunk = self.ui.config('convert', 'svn.trunk')
185 185 cfgbranches = self.ui.config('convert', 'svn.branches')
186 186 cfgtags = self.ui.config('convert', 'svn.tags')
187 187 trunk = (cfgtrunk or 'trunk').strip('/')
188 188 branches = (cfgbranches or 'branches').strip('/')
189 189 tags = (cfgtags or 'tags').strip('/')
190 190 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
191 191 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
192 192 (trunk, branches, tags))
193 193 oldmodule = self.module
194 194 self.module += '/' + trunk
195 195 lt = self.latest(self.module, self.last_changed)
196 196 self.head = self.revid(lt)
197 197 self.heads = [self.head]
198 198 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
199 199 self.ctx)
200 200 for branch in branchnames.keys():
201 201 if oldmodule:
202 202 module = oldmodule + '/' + branches + '/' + branch
203 203 else:
204 204 module = '/' + branches + '/' + branch
205 205 brevnum = self.latest(module, self.last_changed)
206 206 brev = self.revid(brevnum, module)
207 207 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
208 208 self.heads.append(brev)
209 209
210 210 if oldmodule:
211 211 self.tags = '%s/%s' % (oldmodule, tags)
212 212 else:
213 213 self.tags = '/%s' % tags
214 214
215 215 elif cfgtrunk or cfgbranches or cfgtags:
216 216 raise util.Abort('trunk/branch/tags layout expected, but not found')
217 217 else:
218 218 self.ui.note('working with one branch\n')
219 219 self.heads = [self.head]
220 220 self.tags = tags
221 221 return self.heads
222 222
223 223 def getfile(self, file, rev):
224 224 data, mode = self._getfile(file, rev)
225 225 self.modecache[(file, rev)] = mode
226 226 return data
227 227
228 228 def getmode(self, file, rev):
229 229 return self.modecache[(file, rev)]
230 230
231 231 def getchanges(self, rev):
232 232 if self._changescache and self._changescache[0] == rev:
233 233 return self._changescache[1]
234 234 self._changescache = None
235 235 self.modecache = {}
236 236 (paths, parents) = self.paths[rev]
237 237 files, copies = self.expandpaths(rev, paths, parents)
238 238 files.sort()
239 239 files = zip(files, [rev] * len(files))
240 240
241 241 # caller caches the result, so free it here to release memory
242 242 del self.paths[rev]
243 243 return (files, copies)
244 244
245 245 def getchangedfiles(self, rev, i):
246 246 changes = self.getchanges(rev)
247 247 self._changescache = (rev, changes)
248 248 return [f[0] for f in changes[0]]
249 249
250 250 def getcommit(self, rev):
251 251 if rev not in self.commits:
252 252 uuid, module, revnum = self.revsplit(rev)
253 253 self.module = module
254 254 self.reparent(module)
255 255 stop = self.lastrevs.get(module, 0)
256 256 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
257 257 commit = self.commits[rev]
258 258 # caller caches the result, so free it here to release memory
259 259 del self.commits[rev]
260 260 return commit
261 261
262 262 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
263 263 strict_node_history=False):
264 264
265 265 def parent(fp):
266 266 while True:
267 267 entry = pickle.load(fp)
268 268 try:
269 269 orig_paths, revnum, author, date, message = entry
270 270 except:
271 271 if entry is None:
272 272 break
273 273 raise SubversionException("child raised exception", entry)
274 274 yield entry
275 275
276 276 args = [self.url, paths, start, end, limit, discover_changed_paths,
277 277 strict_node_history]
278 278 arg = encodeargs(args)
279 279 hgexe = util.hgexecutable()
280 280 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
281 281 stdin, stdout = os.popen2(cmd, 'b')
282 282
283 283 stdin.write(arg)
284 284 stdin.close()
285 285
286 286 for p in parent(stdout):
287 287 yield p
288 288
289 289 def gettags(self):
290 290 tags = {}
291 291 start = self.revnum(self.head)
292 292 try:
293 293 for entry in self.get_log([self.tags], 0, start):
294 294 orig_paths, revnum, author, date, message = entry
295 295 for path in orig_paths:
296 296 if not path.startswith(self.tags+'/'):
297 297 continue
298 298 ent = orig_paths[path]
299 299 source = ent.copyfrom_path
300 300 rev = ent.copyfrom_rev
301 301 tag = path.split('/')[-1]
302 302 tags[tag] = self.revid(rev, module=source)
303 303 except SubversionException, (inst, num):
304 304 self.ui.note('no tags found at revision %d\n' % start)
305 305 return tags
306 306
307 307 def converted(self, rev, destrev):
308 308 if not self.wc:
309 309 return
310 310 if self.convertfp is None:
311 311 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
312 312 'a')
313 313 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
314 314 self.convertfp.flush()
315 315
316 316 # -- helper functions --
317 317
318 318 def revid(self, revnum, module=None):
319 319 if not module:
320 320 module = self.module
321 321 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
322 322 revnum)
323 323
324 324 def revnum(self, rev):
325 325 return int(rev.split('@')[-1])
326 326
327 327 def revsplit(self, rev):
328 328 url, revnum = rev.encode(self.encoding).split('@', 1)
329 329 revnum = int(revnum)
330 330 parts = url.split('/', 1)
331 331 uuid = parts.pop(0)[4:]
332 332 mod = ''
333 333 if parts:
334 334 mod = '/' + parts[0]
335 335 return uuid, mod, revnum
336 336
337 337 def latest(self, path, stop=0):
338 338 'find the latest revision affecting path, up to stop'
339 339 if not stop:
340 340 stop = svn.ra.get_latest_revnum(self.ra)
341 341 try:
342 342 self.reparent('')
343 343 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
344 344 self.reparent(self.module)
345 345 except SubversionException:
346 346 dirent = None
347 347 if not dirent:
348 348 raise util.Abort('%s not found up to revision %d' % (path, stop))
349 349
350 350 return dirent.created_rev
351 351
352 352 def get_blacklist(self):
353 353 """Avoid certain revision numbers.
354 354 It is not uncommon for two nearby revisions to cancel each other
355 355 out, e.g. 'I copied trunk into a subdirectory of itself instead
356 356 of making a branch'. The converted repository is significantly
357 357 smaller if we ignore such revisions."""
358 358 self.blacklist = util.set()
359 359 blacklist = self.blacklist
360 360 for line in file("blacklist.txt", "r"):
361 361 if not line.startswith("#"):
362 362 try:
363 363 svn_rev = int(line.strip())
364 364 blacklist.add(svn_rev)
365 365 except ValueError, e:
366 366 pass # not an integer or a comment
367 367
368 368 def is_blacklisted(self, svn_rev):
369 369 return svn_rev in self.blacklist
370 370
371 371 def reparent(self, module):
372 372 svn_url = self.base + module
373 373 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
374 374 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
375 375
376 376 def expandpaths(self, rev, paths, parents):
377 377 def get_entry_from_path(path, module=self.module):
378 378 # Given the repository url of this wc, say
379 379 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
380 380 # extract the "entry" portion (a relative path) from what
381 381 # svn log --xml says, ie
382 382 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
383 383 # that is to say "tests/PloneTestCase.py"
384 384 if path.startswith(module):
385 385 relative = path[len(module):]
386 386 if relative.startswith('/'):
387 387 return relative[1:]
388 388 else:
389 389 return relative
390 390
391 391 # The path is outside our tracked tree...
392 392 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
393 393 return None
394 394
395 395 entries = []
396 396 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
397 397 copies = {}
398 398 revnum = self.revnum(rev)
399 399
400 400 if revnum in self.modulemap:
401 401 new_module = self.modulemap[revnum]
402 402 if new_module != self.module:
403 403 self.module = new_module
404 404 self.reparent(self.module)
405 405
406 406 for path, ent in paths:
407 407 entrypath = get_entry_from_path(path, module=self.module)
408 408 entry = entrypath.decode(self.encoding)
409 409
410 410 kind = svn.ra.check_path(self.ra, entrypath, revnum)
411 411 if kind == svn.core.svn_node_file:
412 412 if ent.copyfrom_path:
413 413 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
414 414 if copyfrom_path:
415 415 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
416 416 # It's probably important for hg that the source
417 417 # exists in the revision's parent, not just the
418 418 # ent.copyfrom_rev
419 419 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
420 420 if fromkind != 0:
421 421 copies[self.recode(entry)] = self.recode(copyfrom_path)
422 422 entries.append(self.recode(entry))
423 423 elif kind == 0: # gone, but had better be a deleted *file*
424 424 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
425 425
426 426 # if a branch is created but entries are removed in the same
427 427 # changeset, get the right fromrev
428 428 if parents:
429 429 uuid, old_module, fromrev = self.revsplit(parents[0])
430 430 else:
431 431 fromrev = revnum - 1
432 432 # might always need to be revnum - 1 in these 3 lines?
433 433 old_module = self.modulemap.get(fromrev, self.module)
434 434
435 435 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
436 436 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
437 437
438 438 def lookup_parts(p):
439 439 rc = None
440 440 parts = p.split("/")
441 441 for i in range(len(parts)):
442 442 part = "/".join(parts[:i])
443 443 info = part, copyfrom.get(part, None)
444 444 if info[1] is not None:
445 445 self.ui.debug("Found parent directory %s\n" % info[1])
446 446 rc = info
447 447 return rc
448 448
449 449 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
450 450
451 451 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
452 452
453 453 # need to remove fragment from lookup_parts and replace with copyfrom_path
454 454 if frompath is not None:
455 455 self.ui.debug("munge-o-matic\n")
456 456 self.ui.debug(entrypath + '\n')
457 457 self.ui.debug(entrypath[len(frompath):] + '\n')
458 458 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
459 459 fromrev = froment.copyfrom_rev
460 460 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
461 461
462 462 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
463 463 if fromkind == svn.core.svn_node_file: # a deleted file
464 464 entries.append(self.recode(entry))
465 465 elif fromkind == svn.core.svn_node_dir:
466 466 # print "Deleted/moved non-file:", revnum, path, ent
467 467 # children = self._find_children(path, revnum - 1)
468 468 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
469 469 # Sometimes this is tricky. For example: in
470 470 # The Subversion Repository revision 6940 a dir
471 471 # was copied and one of its files was deleted
472 472 # from the new location in the same commit. This
473 473 # code can't deal with that yet.
474 474 if ent.action == 'C':
475 475 children = self._find_children(path, fromrev)
476 476 else:
477 477 oroot = entrypath.strip('/')
478 478 nroot = path.strip('/')
479 479 children = self._find_children(oroot, fromrev)
480 480 children = [s.replace(oroot,nroot) for s in children]
481 481 # Mark all [files, not directories] as deleted.
482 482 for child in children:
483 483 # Can we move a child directory and its
484 484 # parent in the same commit? (probably can). Could
485 485 # cause problems if instead of revnum -1,
486 486 # we have to look in (copyfrom_path, revnum - 1)
487 487 entrypath = get_entry_from_path("/" + child, module=old_module)
488 488 if entrypath:
489 489 entry = self.recode(entrypath.decode(self.encoding))
490 490 if entry in copies:
491 491 # deleted file within a copy
492 492 del copies[entry]
493 493 else:
494 494 entries.append(entry)
495 495 else:
496 496 self.ui.debug('unknown path in revision %d: %s\n' % \
497 497 (revnum, path))
498 498 elif kind == svn.core.svn_node_dir:
499 499 # Should probably synthesize normal file entries
500 500 # and handle as above to clean up copy/rename handling.
501 501
502 502 # If the directory just had a prop change,
503 503 # then we shouldn't need to look for its children.
504 504 # Also this could create duplicate entries. Not sure
505 505 # whether this will matter. Maybe should make entries a set.
506 506 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
507 507 # This will fail if a directory was copied
508 508 # from another branch and then some of its files
509 509 # were deleted in the same transaction.
510 510 children = self._find_children(path, revnum)
511 511 children.sort()
512 512 for child in children:
513 513 # Can we move a child directory and its
514 514 # parent in the same commit? (probably can). Could
515 515 # cause problems if instead of revnum -1,
516 516 # we have to look in (copyfrom_path, revnum - 1)
517 517 entrypath = get_entry_from_path("/" + child, module=self.module)
518 518 # print child, self.module, entrypath
519 519 if entrypath:
520 520 # Need to filter out directories here...
521 521 kind = svn.ra.check_path(self.ra, entrypath, revnum)
522 522 if kind != svn.core.svn_node_dir:
523 523 entries.append(self.recode(entrypath))
524 524
525 525 # Copies here (must copy all from source)
526 526 # Probably not a real problem for us if
527 527 # source does not exist
528 528
529 529 # Can do this with the copy command "hg copy"
530 530 # if ent.copyfrom_path:
531 531 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
532 532 # module=self.module)
533 533 # copyto_entry = entrypath
534 534 #
535 535 # print "copy directory", copyfrom_entry, 'to', copyto_entry
536 536 #
537 537 # copies.append((copyfrom_entry, copyto_entry))
538 538
539 539 if ent.copyfrom_path:
540 540 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
541 541 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
542 542 if copyfrom_entry:
543 543 copyfrom[path] = ent
544 544 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
545 545
546 546 # Good, /probably/ a regular copy. Really should check
547 547 # to see whether the parent revision actually contains
548 548 # the directory in question.
549 549 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
550 550 children.sort()
551 551 for child in children:
552 552 entrypath = get_entry_from_path("/" + child, module=self.module)
553 553 if entrypath:
554 554 entry = entrypath.decode(self.encoding)
555 555 # print "COPY COPY From", copyfrom_entry, entry
556 556 copyto_path = path + entry[len(copyfrom_entry):]
557 557 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
558 558 # print "COPY", entry, "COPY To", copyto_entry
559 559 copies[self.recode(copyto_entry)] = self.recode(entry)
560 560 # copy from quux splort/quuxfile
561 561
562 562 return (entries, copies)
563 563
564 564 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
565 565 self.child_cset = None
566 566 def parselogentry(orig_paths, revnum, author, date, message):
567 567 self.ui.debug("parsing revision %d (%d changes)\n" %
568 568 (revnum, len(orig_paths)))
569 569
570 570 if revnum in self.modulemap:
571 571 new_module = self.modulemap[revnum]
572 572 if new_module != self.module:
573 573 self.module = new_module
574 574 self.reparent(self.module)
575 575
576 576 rev = self.revid(revnum)
577 577 # branch log might return entries for a parent we already have
578 578 if (rev in self.commits or
579 579 (revnum < self.lastrevs.get(self.module, 0))):
580 580 return
581 581
582 582 parents = []
583 583 # check whether this revision is the start of a branch
584 584 if self.module in orig_paths:
585 585 ent = orig_paths[self.module]
586 586 if ent.copyfrom_path:
587 587 # ent.copyfrom_rev may not be the actual last revision
588 588 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
589 589 self.modulemap[prev] = ent.copyfrom_path
590 590 parents = [self.revid(prev, ent.copyfrom_path)]
591 591 self.ui.note('found parent of branch %s at %d: %s\n' % \
592 592 (self.module, prev, ent.copyfrom_path))
593 593 else:
594 594 self.ui.debug("No copyfrom path, don't know what to do.\n")
595 595
596 596 self.modulemap[revnum] = self.module # track backwards in time
597 597
598 598 orig_paths = orig_paths.items()
599 599 orig_paths.sort()
600 600 paths = []
601 601 # filter out unrelated paths
602 602 for path, ent in orig_paths:
603 603 if not path.startswith(self.module):
604 604 self.ui.debug("boring@%s: %s\n" % (revnum, path))
605 605 continue
606 606 paths.append((path, ent))
607 607
608 608 self.paths[rev] = (paths, parents)
609 609
610 610 # Example SVN datetime. Includes microseconds.
611 611 # ISO-8601 conformant
612 612 # '2007-01-04T17:35:00.902377Z'
613 613 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
614 614
615 615 log = message and self.recode(message)
616 616 author = author and self.recode(author) or ''
617 617 try:
618 618 branch = self.module.split("/")[-1]
619 619 if branch == 'trunk':
620 620 branch = ''
621 621 except IndexError:
622 622 branch = None
623 623
624 624 cset = commit(author=author,
625 625 date=util.datestr(date),
626 626 desc=log,
627 627 parents=parents,
628 628 branch=branch,
629 629 rev=rev.encode('utf-8'))
630 630
631 631 self.commits[rev] = cset
632 632 if self.child_cset and not self.child_cset.parents:
633 633 self.child_cset.parents = [rev]
634 634 self.child_cset = cset
635 635
636 636 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
637 637 (self.module, from_revnum, to_revnum))
638 638
639 639 try:
640 640 for entry in self.get_log([self.module], from_revnum, to_revnum):
641 641 orig_paths, revnum, author, date, message = entry
642 642 if self.is_blacklisted(revnum):
643 643 self.ui.note('skipping blacklisted revision %d\n' % revnum)
644 644 continue
645 645 if orig_paths is None:
646 646 self.ui.debug('revision %d has no entries\n' % revnum)
647 647 continue
648 648 parselogentry(orig_paths, revnum, author, date, message)
649 649 except SubversionException, (inst, num):
650 650 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
651 651 raise NoSuchRevision(branch=self,
652 652 revision="Revision number %d" % to_revnum)
653 653 raise
654 654
655 655 def _getfile(self, file, rev):
656 656 io = StringIO()
657 657 # TODO: ra.get_file transmits the whole file instead of diffs.
658 658 mode = ''
659 659 try:
660 660 revnum = self.revnum(rev)
661 661 if self.module != self.modulemap[revnum]:
662 662 self.module = self.modulemap[revnum]
663 663 self.reparent(self.module)
664 664 info = svn.ra.get_file(self.ra, file, revnum, io)
665 665 if isinstance(info, list):
666 666 info = info[-1]
667 667 mode = ("svn:executable" in info) and 'x' or ''
668 668 mode = ("svn:special" in info) and 'l' or mode
669 669 except SubversionException, e:
670 670 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
671 671 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
672 672 if e.apr_err in notfound: # File not found
673 673 raise IOError()
674 674 raise
675 675 data = io.getvalue()
676 676 if mode == 'l':
677 677 link_prefix = "link "
678 678 if data.startswith(link_prefix):
679 679 data = data[len(link_prefix):]
680 680 return data, mode
681 681
682 682 def _find_children(self, path, revnum):
683 683 path = path.strip('/')
684 684 pool = Pool()
685 685 rpath = '/'.join([self.base, path]).strip('/')
686 686 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
687 687
688 688 pre_revprop_change = '''#!/bin/sh
689 689
690 690 REPOS="$1"
691 691 REV="$2"
692 692 USER="$3"
693 693 PROPNAME="$4"
694 694 ACTION="$5"
695 695
696 696 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
697 697 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
698 698 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
699 699
700 700 echo "Changing prohibited revision property" >&2
701 701 exit 1
702 702 '''
703 703
704 704 class svn_sink(converter_sink, commandline):
705 705 commit_re = re.compile(r'Committed revision (\d+).', re.M)
706 706
707 # iterates sublist of given list for concatenated length is within limit
708 def limit_arglist(self, files):
709 if os.name != 'nt':
710 yield files
711 return
712 # When I tested on WinXP, limit = 2500 is NG, 2400 is OK
713 limit = 2000
714 bytes = 0
715 fl = []
716 for fn in files:
717 b = len(fn) + 1
718 if bytes + b < limit:
719 fl.append(fn)
720 bytes += b
721 else:
722 yield fl
723 fl = []
724 bytes = 0
725 if fl:
726 yield fl
727
707 728 def prerun(self):
708 729 if self.wc:
709 730 os.chdir(self.wc)
710 731
711 732 def postrun(self):
712 733 if self.wc:
713 734 os.chdir(self.cwd)
714 735
715 736 def join(self, name):
716 737 return os.path.join(self.wc, '.svn', name)
717 738
718 739 def revmapfile(self):
719 740 return self.join('hg-shamap')
720 741
721 742 def authorfile(self):
722 743 return self.join('hg-authormap')
723 744
724 745 def __init__(self, ui, path):
725 746 converter_sink.__init__(self, ui, path)
726 747 commandline.__init__(self, ui, 'svn')
727 748 self.delete = []
728 749 self.setexec = []
729 750 self.delexec = []
730 751 self.copies = []
731 752 self.wc = None
732 753 self.cwd = os.getcwd()
733 754
734 755 path = os.path.realpath(path)
735 756
736 757 created = False
737 758 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
738 759 self.wc = path
739 760 self.run0('update')
740 761 else:
741 762 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
742 763
743 764 if os.path.isdir(os.path.dirname(path)):
744 765 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
745 766 ui.status(_('initializing svn repo %r\n') %
746 767 os.path.basename(path))
747 768 commandline(ui, 'svnadmin').run0('create', path)
748 769 created = path
749 770 path = path.replace('\\', '/')
750 771 if not path.startswith('/'):
751 772 path = '/' + path
752 773 path = 'file://' + path
753 774
754 775 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
755 776 self.run0('checkout', path, wcpath)
756 777
757 778 self.wc = wcpath
758 779 self.opener = util.opener(self.wc)
759 780 self.wopener = util.opener(self.wc)
760 781 self.childmap = mapfile(ui, self.join('hg-childmap'))
761 782 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
762 783
763 784 if created:
764 785 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
765 786 fp = open(hook, 'w')
766 787 fp.write(pre_revprop_change)
767 788 fp.close()
768 789 util.set_flags(hook, "x")
769 790
770 791 xport = transport.SvnRaTransport(url=geturl(path))
771 792 self.uuid = svn.ra.get_uuid(xport.ra)
772 793
773 794 def wjoin(self, *names):
774 795 return os.path.join(self.wc, *names)
775 796
776 797 def putfile(self, filename, flags, data):
777 798 if 'l' in flags:
778 799 self.wopener.symlink(data, filename)
779 800 else:
780 801 try:
781 802 if os.path.islink(self.wjoin(filename)):
782 803 os.unlink(filename)
783 804 except OSError:
784 805 pass
785 806 self.wopener(filename, 'w').write(data)
786 807
787 808 if self.is_exec:
788 809 was_exec = self.is_exec(self.wjoin(filename))
789 810 else:
790 811 # On filesystems not supporting execute-bit, there is no way
791 812 # to know if it is set but asking subversion. Setting it
792 813 # systematically is just as expensive and much simpler.
793 814 was_exec = 'x' not in flags
794 815
795 816 util.set_flags(self.wjoin(filename), flags)
796 817 if was_exec:
797 818 if 'x' not in flags:
798 819 self.delexec.append(filename)
799 820 else:
800 821 if 'x' in flags:
801 822 self.setexec.append(filename)
802 823
803 824 def delfile(self, name):
804 825 self.delete.append(name)
805 826
806 827 def copyfile(self, source, dest):
807 828 self.copies.append([source, dest])
808 829
809 830 def _copyfile(self, source, dest):
810 831 # SVN's copy command pukes if the destination file exists, but
811 832 # our copyfile method expects to record a copy that has
812 833 # already occurred. Cross the semantic gap.
813 834 wdest = self.wjoin(dest)
814 835 exists = os.path.exists(wdest)
815 836 if exists:
816 837 fd, tempname = tempfile.mkstemp(
817 838 prefix='hg-copy-', dir=os.path.dirname(wdest))
818 839 os.close(fd)
819 840 os.unlink(tempname)
820 841 os.rename(wdest, tempname)
821 842 try:
822 843 self.run0('copy', source, dest)
823 844 finally:
824 845 if exists:
825 846 try:
826 847 os.unlink(wdest)
827 848 except OSError:
828 849 pass
829 850 os.rename(tempname, wdest)
830 851
831 852 def dirs_of(self, files):
832 853 dirs = set()
833 854 for f in files:
834 855 if os.path.isdir(self.wjoin(f)):
835 856 dirs.add(f)
836 857 for i in strutil.rfindall(f, '/'):
837 858 dirs.add(f[:i])
838 859 return dirs
839 860
840 861 def add_dirs(self, files):
841 862 add_dirs = [d for d in self.dirs_of(files)
842 863 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
843 864 if add_dirs:
844 865 add_dirs.sort()
845 self.run('add', non_recursive=True, quiet=True, *add_dirs)
866 for fl in self.limit_arglist(add_dirs):
867 self.run('add', non_recursive=True, quiet=True, *fl)
846 868 return add_dirs
847 869
848 870 def add_files(self, files):
849 871 if files:
850 self.run('add', quiet=True, *files)
872 for fl in self.limit_arglist(files):
873 self.run('add', quiet=True, *fl)
851 874 return files
852 875
853 876 def tidy_dirs(self, names):
854 877 dirs = list(self.dirs_of(names))
855 878 dirs.sort(reverse=True)
856 879 deleted = []
857 880 for d in dirs:
858 881 wd = self.wjoin(d)
859 882 if os.listdir(wd) == '.svn':
860 883 self.run0('delete', d)
861 884 deleted.append(d)
862 885 return deleted
863 886
864 887 def addchild(self, parent, child):
865 888 self.childmap[parent] = child
866 889
867 890 def revid(self, rev):
868 891 return u"svn:%s@%s" % (self.uuid, rev)
869 892
870 893 def putcommit(self, files, parents, commit):
871 894 for parent in parents:
872 895 try:
873 896 return self.revid(self.childmap[parent])
874 897 except KeyError:
875 898 pass
876 899 entries = set(self.delete)
877 900 files = util.frozenset(files)
878 901 entries.update(self.add_dirs(files.difference(entries)))
879 902 if self.copies:
880 903 for s, d in self.copies:
881 904 self._copyfile(s, d)
882 905 self.copies = []
883 906 if self.delete:
884 self.run0('delete', *self.delete)
907 for fl in self.limit_arglist(self.delete):
908 self.run0('delete', *fl)
885 909 self.delete = []
886 910 entries.update(self.add_files(files.difference(entries)))
887 911 entries.update(self.tidy_dirs(entries))
888 912 if self.delexec:
889 self.run0('propdel', 'svn:executable', *self.delexec)
913 for fl in self.limit_arglist(self.delexec):
914 self.run0('propdel', 'svn:executable', *fl)
890 915 self.delexec = []
891 916 if self.setexec:
892 self.run0('propset', 'svn:executable', '*', *self.setexec)
917 for fl in self.limit_arglist(self.setexec):
918 self.run0('propset', 'svn:executable', '*', *fl)
893 919 self.setexec = []
894 920
895 921 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
896 922 fp = os.fdopen(fd, 'w')
897 923 fp.write(commit.desc)
898 924 fp.close()
899 925 try:
900 926 output = self.run0('commit',
901 927 username=util.shortuser(commit.author),
902 928 file=messagefile,
903 encoding='utf-8',
904 *list(entries))
929 encoding='utf-8')
905 930 try:
906 931 rev = self.commit_re.search(output).group(1)
907 932 except AttributeError:
908 933 self.ui.warn(_('unexpected svn output:\n'))
909 934 self.ui.warn(output)
910 935 raise util.Abort(_('unable to cope with svn output'))
911 936 if commit.rev:
912 937 self.run('propset', 'hg:convert-rev', commit.rev,
913 938 revprop=True, revision=rev)
914 939 if commit.branch and commit.branch != 'default':
915 940 self.run('propset', 'hg:convert-branch', commit.branch,
916 941 revprop=True, revision=rev)
917 942 for parent in parents:
918 943 self.addchild(parent, rev)
919 944 return self.revid(rev)
920 945 finally:
921 946 os.unlink(messagefile)
922 947
923 948 def puttags(self, tags):
924 949 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now