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