##// END OF EJS Templates
convert: fix svn file:// URL generation under Windows
Patrick Mezard -
r5535:7501ef26 default
parent child Browse files
Show More
@@ -1,866 +1,871
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 def setrevmap(self, revmap):
158 158 lastrevs = {}
159 159 for revid in revmap.iterkeys():
160 160 uuid, module, revnum = self.revsplit(revid)
161 161 lastrevnum = lastrevs.setdefault(module, revnum)
162 162 if revnum > lastrevnum:
163 163 lastrevs[module] = revnum
164 164 self.lastrevs = lastrevs
165 165
166 166 def exists(self, path, optrev):
167 167 try:
168 168 svn.client.ls(self.url.rstrip('/') + '/' + path,
169 169 optrev, False, self.ctx)
170 170 return True
171 171 except SubversionException, err:
172 172 return False
173 173
174 174 def getheads(self):
175 175 # detect standard /branches, /tags, /trunk layout
176 176 rev = optrev(self.last_changed)
177 177 rpath = self.url.strip('/')
178 178 cfgtrunk = self.ui.config('convert', 'svn.trunk')
179 179 cfgbranches = self.ui.config('convert', 'svn.branches')
180 180 cfgtags = self.ui.config('convert', 'svn.tags')
181 181 trunk = (cfgtrunk or 'trunk').strip('/')
182 182 branches = (cfgbranches or 'branches').strip('/')
183 183 tags = (cfgtags or 'tags').strip('/')
184 184 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
185 185 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
186 186 (trunk, branches, tags))
187 187 oldmodule = self.module
188 188 self.module += '/' + trunk
189 189 lt = self.latest(self.module, self.last_changed)
190 190 self.head = self.revid(lt)
191 191 self.heads = [self.head]
192 192 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
193 193 self.ctx)
194 194 for branch in branchnames.keys():
195 195 if oldmodule:
196 196 module = oldmodule + '/' + branches + '/' + branch
197 197 else:
198 198 module = '/' + branches + '/' + branch
199 199 brevnum = self.latest(module, self.last_changed)
200 200 brev = self.revid(brevnum, module)
201 201 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
202 202 self.heads.append(brev)
203 203
204 204 if oldmodule:
205 205 self.tags = '%s/%s' % (oldmodule, tags)
206 206 else:
207 207 self.tags = '/%s' % tags
208 208
209 209 elif cfgtrunk or cfgbranches or cfgtags:
210 210 raise util.Abort('trunk/branch/tags layout expected, but not found')
211 211 else:
212 212 self.ui.note('working with one branch\n')
213 213 self.heads = [self.head]
214 214 self.tags = tags
215 215 return self.heads
216 216
217 217 def getfile(self, file, rev):
218 218 data, mode = self._getfile(file, rev)
219 219 self.modecache[(file, rev)] = mode
220 220 return data
221 221
222 222 def getmode(self, file, rev):
223 223 return self.modecache[(file, rev)]
224 224
225 225 def getchanges(self, rev):
226 226 if self._changescache and self._changescache[0] == rev:
227 227 return self._changescache[1]
228 228 self._changescache = None
229 229 self.modecache = {}
230 230 (paths, parents) = self.paths[rev]
231 231 files, copies = self.expandpaths(rev, paths, parents)
232 232 files.sort()
233 233 files = zip(files, [rev] * len(files))
234 234
235 235 # caller caches the result, so free it here to release memory
236 236 del self.paths[rev]
237 237 return (files, copies)
238 238
239 239 def getchangedfiles(self, rev, i):
240 240 changes = self.getchanges(rev)
241 241 self._changescache = (rev, changes)
242 242 return [f[0] for f in changes[0]]
243 243
244 244 def getcommit(self, rev):
245 245 if rev not in self.commits:
246 246 uuid, module, revnum = self.revsplit(rev)
247 247 self.module = module
248 248 self.reparent(module)
249 249 stop = self.lastrevs.get(module, 0)
250 250 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
251 251 commit = self.commits[rev]
252 252 # caller caches the result, so free it here to release memory
253 253 del self.commits[rev]
254 254 return commit
255 255
256 256 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
257 257 strict_node_history=False):
258 258
259 259 def parent(fp):
260 260 while True:
261 261 entry = pickle.load(fp)
262 262 try:
263 263 orig_paths, revnum, author, date, message = entry
264 264 except:
265 265 if entry is None:
266 266 break
267 267 raise SubversionException("child raised exception", entry)
268 268 yield entry
269 269
270 270 args = [self.url, paths, start, end, limit, discover_changed_paths,
271 271 strict_node_history]
272 272 arg = encodeargs(args)
273 273 hgexe = util.hgexecutable()
274 274 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
275 275 stdin, stdout = os.popen2(cmd, 'b')
276 276
277 277 stdin.write(arg)
278 278 stdin.close()
279 279
280 280 for p in parent(stdout):
281 281 yield p
282 282
283 283 def gettags(self):
284 284 tags = {}
285 285 start = self.revnum(self.head)
286 286 try:
287 287 for entry in self.get_log([self.tags], 0, start):
288 288 orig_paths, revnum, author, date, message = entry
289 289 for path in orig_paths:
290 290 if not path.startswith(self.tags+'/'):
291 291 continue
292 292 ent = orig_paths[path]
293 293 source = ent.copyfrom_path
294 294 rev = ent.copyfrom_rev
295 295 tag = path.split('/')[-1]
296 296 tags[tag] = self.revid(rev, module=source)
297 297 except SubversionException, (inst, num):
298 298 self.ui.note('no tags found at revision %d\n' % start)
299 299 return tags
300 300
301 301 # -- helper functions --
302 302
303 303 def revid(self, revnum, module=None):
304 304 if not module:
305 305 module = self.module
306 306 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
307 307 revnum)
308 308
309 309 def revnum(self, rev):
310 310 return int(rev.split('@')[-1])
311 311
312 312 def revsplit(self, rev):
313 313 url, revnum = rev.encode(self.encoding).split('@', 1)
314 314 revnum = int(revnum)
315 315 parts = url.split('/', 1)
316 316 uuid = parts.pop(0)[4:]
317 317 mod = ''
318 318 if parts:
319 319 mod = '/' + parts[0]
320 320 return uuid, mod, revnum
321 321
322 322 def latest(self, path, stop=0):
323 323 'find the latest revision affecting path, up to stop'
324 324 if not stop:
325 325 stop = svn.ra.get_latest_revnum(self.ra)
326 326 try:
327 327 self.reparent('')
328 328 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
329 329 self.reparent(self.module)
330 330 except SubversionException:
331 331 dirent = None
332 332 if not dirent:
333 333 raise util.Abort('%s not found up to revision %d' % (path, stop))
334 334
335 335 return dirent.created_rev
336 336
337 337 def get_blacklist(self):
338 338 """Avoid certain revision numbers.
339 339 It is not uncommon for two nearby revisions to cancel each other
340 340 out, e.g. 'I copied trunk into a subdirectory of itself instead
341 341 of making a branch'. The converted repository is significantly
342 342 smaller if we ignore such revisions."""
343 343 self.blacklist = util.set()
344 344 blacklist = self.blacklist
345 345 for line in file("blacklist.txt", "r"):
346 346 if not line.startswith("#"):
347 347 try:
348 348 svn_rev = int(line.strip())
349 349 blacklist.add(svn_rev)
350 350 except ValueError, e:
351 351 pass # not an integer or a comment
352 352
353 353 def is_blacklisted(self, svn_rev):
354 354 return svn_rev in self.blacklist
355 355
356 356 def reparent(self, module):
357 357 svn_url = self.base + module
358 358 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
359 359 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
360 360
361 361 def expandpaths(self, rev, paths, parents):
362 362 def get_entry_from_path(path, module=self.module):
363 363 # Given the repository url of this wc, say
364 364 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
365 365 # extract the "entry" portion (a relative path) from what
366 366 # svn log --xml says, ie
367 367 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
368 368 # that is to say "tests/PloneTestCase.py"
369 369 if path.startswith(module):
370 370 relative = path[len(module):]
371 371 if relative.startswith('/'):
372 372 return relative[1:]
373 373 else:
374 374 return relative
375 375
376 376 # The path is outside our tracked tree...
377 377 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
378 378 return None
379 379
380 380 entries = []
381 381 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
382 382 copies = {}
383 383 revnum = self.revnum(rev)
384 384
385 385 if revnum in self.modulemap:
386 386 new_module = self.modulemap[revnum]
387 387 if new_module != self.module:
388 388 self.module = new_module
389 389 self.reparent(self.module)
390 390
391 391 for path, ent in paths:
392 392 entrypath = get_entry_from_path(path, module=self.module)
393 393 entry = entrypath.decode(self.encoding)
394 394
395 395 kind = svn.ra.check_path(self.ra, entrypath, revnum)
396 396 if kind == svn.core.svn_node_file:
397 397 if ent.copyfrom_path:
398 398 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
399 399 if copyfrom_path:
400 400 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
401 401 # It's probably important for hg that the source
402 402 # exists in the revision's parent, not just the
403 403 # ent.copyfrom_rev
404 404 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
405 405 if fromkind != 0:
406 406 copies[self.recode(entry)] = self.recode(copyfrom_path)
407 407 entries.append(self.recode(entry))
408 408 elif kind == 0: # gone, but had better be a deleted *file*
409 409 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
410 410
411 411 # if a branch is created but entries are removed in the same
412 412 # changeset, get the right fromrev
413 413 if parents:
414 414 uuid, old_module, fromrev = self.revsplit(parents[0])
415 415 else:
416 416 fromrev = revnum - 1
417 417 # might always need to be revnum - 1 in these 3 lines?
418 418 old_module = self.modulemap.get(fromrev, self.module)
419 419
420 420 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
421 421 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
422 422
423 423 def lookup_parts(p):
424 424 rc = None
425 425 parts = p.split("/")
426 426 for i in range(len(parts)):
427 427 part = "/".join(parts[:i])
428 428 info = part, copyfrom.get(part, None)
429 429 if info[1] is not None:
430 430 self.ui.debug("Found parent directory %s\n" % info[1])
431 431 rc = info
432 432 return rc
433 433
434 434 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
435 435
436 436 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
437 437
438 438 # need to remove fragment from lookup_parts and replace with copyfrom_path
439 439 if frompath is not None:
440 440 self.ui.debug("munge-o-matic\n")
441 441 self.ui.debug(entrypath + '\n')
442 442 self.ui.debug(entrypath[len(frompath):] + '\n')
443 443 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
444 444 fromrev = froment.copyfrom_rev
445 445 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
446 446
447 447 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
448 448 if fromkind == svn.core.svn_node_file: # a deleted file
449 449 entries.append(self.recode(entry))
450 450 elif fromkind == svn.core.svn_node_dir:
451 451 # print "Deleted/moved non-file:", revnum, path, ent
452 452 # children = self._find_children(path, revnum - 1)
453 453 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
454 454 # Sometimes this is tricky. For example: in
455 455 # The Subversion Repository revision 6940 a dir
456 456 # was copied and one of its files was deleted
457 457 # from the new location in the same commit. This
458 458 # code can't deal with that yet.
459 459 if ent.action == 'C':
460 460 children = self._find_children(path, fromrev)
461 461 else:
462 462 oroot = entrypath.strip('/')
463 463 nroot = path.strip('/')
464 464 children = self._find_children(oroot, fromrev)
465 465 children = [s.replace(oroot,nroot) for s in children]
466 466 # Mark all [files, not directories] as deleted.
467 467 for child in children:
468 468 # Can we move a child directory and its
469 469 # parent in the same commit? (probably can). Could
470 470 # cause problems if instead of revnum -1,
471 471 # we have to look in (copyfrom_path, revnum - 1)
472 472 entrypath = get_entry_from_path("/" + child, module=old_module)
473 473 if entrypath:
474 474 entry = self.recode(entrypath.decode(self.encoding))
475 475 if entry in copies:
476 476 # deleted file within a copy
477 477 del copies[entry]
478 478 else:
479 479 entries.append(entry)
480 480 else:
481 481 self.ui.debug('unknown path in revision %d: %s\n' % \
482 482 (revnum, path))
483 483 elif kind == svn.core.svn_node_dir:
484 484 # Should probably synthesize normal file entries
485 485 # and handle as above to clean up copy/rename handling.
486 486
487 487 # If the directory just had a prop change,
488 488 # then we shouldn't need to look for its children.
489 489 # Also this could create duplicate entries. Not sure
490 490 # whether this will matter. Maybe should make entries a set.
491 491 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
492 492 # This will fail if a directory was copied
493 493 # from another branch and then some of its files
494 494 # were deleted in the same transaction.
495 495 children = self._find_children(path, revnum)
496 496 children.sort()
497 497 for child in children:
498 498 # Can we move a child directory and its
499 499 # parent in the same commit? (probably can). Could
500 500 # cause problems if instead of revnum -1,
501 501 # we have to look in (copyfrom_path, revnum - 1)
502 502 entrypath = get_entry_from_path("/" + child, module=self.module)
503 503 # print child, self.module, entrypath
504 504 if entrypath:
505 505 # Need to filter out directories here...
506 506 kind = svn.ra.check_path(self.ra, entrypath, revnum)
507 507 if kind != svn.core.svn_node_dir:
508 508 entries.append(self.recode(entrypath))
509 509
510 510 # Copies here (must copy all from source)
511 511 # Probably not a real problem for us if
512 512 # source does not exist
513 513
514 514 # Can do this with the copy command "hg copy"
515 515 # if ent.copyfrom_path:
516 516 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
517 517 # module=self.module)
518 518 # copyto_entry = entrypath
519 519 #
520 520 # print "copy directory", copyfrom_entry, 'to', copyto_entry
521 521 #
522 522 # copies.append((copyfrom_entry, copyto_entry))
523 523
524 524 if ent.copyfrom_path:
525 525 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
526 526 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
527 527 if copyfrom_entry:
528 528 copyfrom[path] = ent
529 529 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
530 530
531 531 # Good, /probably/ a regular copy. Really should check
532 532 # to see whether the parent revision actually contains
533 533 # the directory in question.
534 534 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
535 535 children.sort()
536 536 for child in children:
537 537 entrypath = get_entry_from_path("/" + child, module=self.module)
538 538 if entrypath:
539 539 entry = entrypath.decode(self.encoding)
540 540 # print "COPY COPY From", copyfrom_entry, entry
541 541 copyto_path = path + entry[len(copyfrom_entry):]
542 542 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
543 543 # print "COPY", entry, "COPY To", copyto_entry
544 544 copies[self.recode(copyto_entry)] = self.recode(entry)
545 545 # copy from quux splort/quuxfile
546 546
547 547 return (entries, copies)
548 548
549 549 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
550 550 self.child_cset = None
551 551 def parselogentry(orig_paths, revnum, author, date, message):
552 552 self.ui.debug("parsing revision %d (%d changes)\n" %
553 553 (revnum, len(orig_paths)))
554 554
555 555 if revnum in self.modulemap:
556 556 new_module = self.modulemap[revnum]
557 557 if new_module != self.module:
558 558 self.module = new_module
559 559 self.reparent(self.module)
560 560
561 561 rev = self.revid(revnum)
562 562 # branch log might return entries for a parent we already have
563 563 if (rev in self.commits or
564 564 (revnum < self.lastrevs.get(self.module, 0))):
565 565 return
566 566
567 567 parents = []
568 568 # check whether this revision is the start of a branch
569 569 if self.module in orig_paths:
570 570 ent = orig_paths[self.module]
571 571 if ent.copyfrom_path:
572 572 # ent.copyfrom_rev may not be the actual last revision
573 573 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
574 574 self.modulemap[prev] = ent.copyfrom_path
575 575 parents = [self.revid(prev, ent.copyfrom_path)]
576 576 self.ui.note('found parent of branch %s at %d: %s\n' % \
577 577 (self.module, prev, ent.copyfrom_path))
578 578 else:
579 579 self.ui.debug("No copyfrom path, don't know what to do.\n")
580 580
581 581 self.modulemap[revnum] = self.module # track backwards in time
582 582
583 583 orig_paths = orig_paths.items()
584 584 orig_paths.sort()
585 585 paths = []
586 586 # filter out unrelated paths
587 587 for path, ent in orig_paths:
588 588 if not path.startswith(self.module):
589 589 self.ui.debug("boring@%s: %s\n" % (revnum, path))
590 590 continue
591 591 paths.append((path, ent))
592 592
593 593 self.paths[rev] = (paths, parents)
594 594
595 595 # Example SVN datetime. Includes microseconds.
596 596 # ISO-8601 conformant
597 597 # '2007-01-04T17:35:00.902377Z'
598 598 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
599 599
600 600 log = message and self.recode(message)
601 601 author = author and self.recode(author) or ''
602 602 try:
603 603 branch = self.module.split("/")[-1]
604 604 if branch == 'trunk':
605 605 branch = ''
606 606 except IndexError:
607 607 branch = None
608 608
609 609 cset = commit(author=author,
610 610 date=util.datestr(date),
611 611 desc=log,
612 612 parents=parents,
613 613 branch=branch,
614 614 rev=rev.encode('utf-8'))
615 615
616 616 self.commits[rev] = cset
617 617 if self.child_cset and not self.child_cset.parents:
618 618 self.child_cset.parents = [rev]
619 619 self.child_cset = cset
620 620
621 621 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
622 622 (self.module, from_revnum, to_revnum))
623 623
624 624 try:
625 625 for entry in self.get_log([self.module], from_revnum, to_revnum):
626 626 orig_paths, revnum, author, date, message = entry
627 627 if self.is_blacklisted(revnum):
628 628 self.ui.note('skipping blacklisted revision %d\n' % revnum)
629 629 continue
630 630 if orig_paths is None:
631 631 self.ui.debug('revision %d has no entries\n' % revnum)
632 632 continue
633 633 parselogentry(orig_paths, revnum, author, date, message)
634 634 except SubversionException, (inst, num):
635 635 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
636 636 raise NoSuchRevision(branch=self,
637 637 revision="Revision number %d" % to_revnum)
638 638 raise
639 639
640 640 def _getfile(self, file, rev):
641 641 io = StringIO()
642 642 # TODO: ra.get_file transmits the whole file instead of diffs.
643 643 mode = ''
644 644 try:
645 645 revnum = self.revnum(rev)
646 646 if self.module != self.modulemap[revnum]:
647 647 self.module = self.modulemap[revnum]
648 648 self.reparent(self.module)
649 649 info = svn.ra.get_file(self.ra, file, revnum, io)
650 650 if isinstance(info, list):
651 651 info = info[-1]
652 652 mode = ("svn:executable" in info) and 'x' or ''
653 653 mode = ("svn:special" in info) and 'l' or mode
654 654 except SubversionException, e:
655 655 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
656 656 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
657 657 if e.apr_err in notfound: # File not found
658 658 raise IOError()
659 659 raise
660 660 data = io.getvalue()
661 661 if mode == 'l':
662 662 link_prefix = "link "
663 663 if data.startswith(link_prefix):
664 664 data = data[len(link_prefix):]
665 665 return data, mode
666 666
667 667 def _find_children(self, path, revnum):
668 668 path = path.strip('/')
669 669 pool = Pool()
670 670 rpath = '/'.join([self.base, path]).strip('/')
671 671 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
672 672
673 673 pre_revprop_change = '''#!/bin/sh
674 674
675 675 REPOS="$1"
676 676 REV="$2"
677 677 USER="$3"
678 678 PROPNAME="$4"
679 679 ACTION="$5"
680 680
681 681 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
682 682 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
683 683 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
684 684
685 685 echo "Changing prohibited revision property" >&2
686 686 exit 1
687 687 '''
688 688
689 689 class svn_sink(converter_sink, commandline):
690 690 commit_re = re.compile(r'Committed revision (\d+).', re.M)
691 691
692 692 def prerun(self):
693 693 if self.wc:
694 694 os.chdir(self.wc)
695 695
696 696 def postrun(self):
697 697 if self.wc:
698 698 os.chdir(self.cwd)
699 699
700 700 def join(self, name):
701 701 return os.path.join(self.wc, '.svn', name)
702 702
703 703 def revmapfile(self):
704 704 return self.join('hg-shamap')
705 705
706 706 def authorfile(self):
707 707 return self.join('hg-authormap')
708 708
709 709 def __init__(self, ui, path):
710 710 converter_sink.__init__(self, ui, path)
711 711 commandline.__init__(self, ui, 'svn')
712 712 self.delete = []
713 713 self.wc = None
714 714 self.cwd = os.getcwd()
715 715
716 716 path = os.path.realpath(path)
717 717
718 718 created = False
719 719 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
720 720 self.wc = path
721 721 self.run0('update')
722 722 else:
723 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
724
723 725 if os.path.isdir(os.path.dirname(path)):
724 726 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
725 727 ui.status(_('initializing svn repo %r\n') %
726 728 os.path.basename(path))
727 729 commandline(ui, 'svnadmin').run0('create', path)
728 730 created = path
731 path = path.replace('\\', '/')
732 if not path.startswith('/'):
733 path = '/' + path
729 734 path = 'file://' + path
730 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
735
731 736 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
732 737 self.run0('checkout', path, wcpath)
733 738
734 739 self.wc = wcpath
735 740 self.opener = util.opener(self.wc)
736 741 self.wopener = util.opener(self.wc)
737 742 self.childmap = mapfile(ui, self.join('hg-childmap'))
738 743
739 744 if created:
740 745 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
741 746 fp = open(hook, 'w')
742 747 fp.write(pre_revprop_change)
743 748 fp.close()
744 749 util.set_exec(hook, True)
745 750
746 751 def wjoin(self, *names):
747 752 return os.path.join(self.wc, *names)
748 753
749 754 def putfile(self, filename, flags, data):
750 755 if 'l' in flags:
751 756 self.wopener.symlink(data, filename)
752 757 else:
753 758 try:
754 759 if os.path.islink(self.wjoin(filename)):
755 760 os.unlink(filename)
756 761 except OSError:
757 762 pass
758 763 self.wopener(filename, 'w').write(data)
759 764 was_exec = util.is_exec(self.wjoin(filename))
760 765 util.set_exec(self.wjoin(filename), 'x' in flags)
761 766 if was_exec:
762 767 if 'x' not in flags:
763 768 self.run0('propdel', 'svn:executable', filename)
764 769 else:
765 770 if 'x' in flags:
766 771 self.run0('propset', 'svn:executable', '*', filename)
767 772
768 773 def delfile(self, name):
769 774 self.delete.append(name)
770 775
771 776 def copyfile(self, source, dest):
772 777 # SVN's copy command pukes if the destination file exists, but
773 778 # our copyfile method expects to record a copy that has
774 779 # already occurred. Cross the semantic gap.
775 780 wdest = self.wjoin(dest)
776 781 exists = os.path.exists(wdest)
777 782 if exists:
778 783 fd, tempname = tempfile.mkstemp(
779 784 prefix='hg-copy-', dir=os.path.dirname(wdest))
780 785 os.close(fd)
781 786 os.unlink(tempname)
782 787 os.rename(wdest, tempname)
783 788 try:
784 789 self.run0('copy', source, dest)
785 790 finally:
786 791 if exists:
787 792 try:
788 793 os.unlink(wdest)
789 794 except OSError:
790 795 pass
791 796 os.rename(tempname, wdest)
792 797
793 798 def dirs_of(self, files):
794 799 dirs = set()
795 800 for f in files:
796 801 if os.path.isdir(self.wjoin(f)):
797 802 dirs.add(f)
798 803 for i in strutil.rfindall(f, '/'):
799 804 dirs.add(f[:i])
800 805 return dirs
801 806
802 807 def add_files(self, files):
803 808 add_dirs = [d for d in self.dirs_of(files)
804 809 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
805 810 if add_dirs:
806 811 self.run('add', non_recursive=True, quiet=True, *add)
807 812 if files:
808 813 self.run('add', quiet=True, *files)
809 814 return files.union(add_dirs)
810 815
811 816 def tidy_dirs(self, names):
812 817 dirs = list(self.dirs_of(names))
813 818 dirs.sort(reverse=True)
814 819 deleted = []
815 820 for d in dirs:
816 821 wd = self.wjoin(d)
817 822 if os.listdir(wd) == '.svn':
818 823 self.run0('delete', d)
819 824 deleted.append(d)
820 825 return deleted
821 826
822 827 def addchild(self, parent, child):
823 828 self.childmap[parent] = child
824 829
825 830 def putcommit(self, files, parents, commit):
826 831 for parent in parents:
827 832 try:
828 833 return self.childmap[parent]
829 834 except KeyError:
830 835 pass
831 836 entries = set(self.delete)
832 837 if self.delete:
833 838 self.run0('delete', *self.delete)
834 839 self.delete = []
835 840 files = util.frozenset(files)
836 841 entries.update(self.add_files(files.difference(entries)))
837 842 entries.update(self.tidy_dirs(entries))
838 843 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
839 844 fp = os.fdopen(fd, 'w')
840 845 fp.write(commit.desc)
841 846 fp.close()
842 847 try:
843 848 output = self.run0('commit',
844 849 username=util.shortuser(commit.author),
845 850 file=messagefile,
846 851 *list(entries))
847 852 try:
848 853 rev = self.commit_re.search(output).group(1)
849 854 except AttributeError:
850 855 self.ui.warn(_('unexpected svn output:\n'))
851 856 self.ui.warn(output)
852 857 raise util.Abort(_('unable to cope with svn output'))
853 858 if commit.rev:
854 859 self.run('propset', 'hg:convert-rev', commit.rev,
855 860 revprop=True, revision=rev)
856 861 if commit.branch and commit.branch != 'default':
857 862 self.run('propset', 'hg:convert-branch', commit.branch,
858 863 revprop=True, revision=rev)
859 864 for parent in parents:
860 865 self.addchild(parent, rev)
861 866 return rev
862 867 finally:
863 868 os.unlink(messagefile)
864 869
865 870 def puttags(self, tags):
866 871 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now