comparison hgext/convert/subversion.py @ 4795:09dae950919f

convert: svn: autodetect /branches, /tags, /trunk. Various other branch handling improvement attempts too.
author Brendan Cully <brendan@kublai.com>
date Tue, 03 Jul 2007 19:26:41 -0700
parents 26857a6f9dd0
children 83c1bbb934ec
comparison
equal deleted inserted replaced
4794:26857a6f9dd0 4795:09dae950919f
24 import transport 24 import transport
25 except ImportError: 25 except ImportError:
26 pass 26 pass
27 27
28 class CompatibilityException(Exception): pass 28 class CompatibilityException(Exception): pass
29
30 LOG_BATCH_SIZE = 50
31 29
32 class svn_entry(object): 30 class svn_entry(object):
33 """Emulate a Subversion path change.""" 31 """Emulate a Subversion path change."""
34 __slots__ = ['path', 'copyfrom_path', 'copyfrom_rev', 'action'] 32 __slots__ = ['path', 'copyfrom_path', 'copyfrom_rev', 'action']
35 def __init__(self, entry): 33 def __init__(self, entry):
104 self.url = url 102 self.url = url
105 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8 103 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
106 try: 104 try:
107 self.transport = transport.SvnRaTransport(url = url) 105 self.transport = transport.SvnRaTransport(url = url)
108 self.ra = self.transport.ra 106 self.ra = self.transport.ra
107 self.ctx = svn.client.create_context()
109 self.base = svn.ra.get_repos_root(self.ra) 108 self.base = svn.ra.get_repos_root(self.ra)
110 self.module = self.url[len(self.base):] 109 self.module = self.url[len(self.base):]
111 self.modulemap = {} # revision, module 110 self.modulemap = {} # revision, module
112 self.commits = {} 111 self.commits = {}
113 self.files = {} 112 self.files = {}
135 def revsplit(self, rev): 134 def revsplit(self, rev):
136 url, revnum = rev.encode(self.encoding).split('@', 1) 135 url, revnum = rev.encode(self.encoding).split('@', 1)
137 revnum = int(revnum) 136 revnum = int(revnum)
138 parts = url.split('/', 1) 137 parts = url.split('/', 1)
139 uuid = parts.pop(0)[4:] 138 uuid = parts.pop(0)[4:]
140 mod = '/' 139 mod = ''
141 if parts: 140 if parts:
142 mod += parts[0] 141 mod = '/' + parts[0]
143 return uuid, mod, revnum 142 return uuid, mod, revnum
144 143
145 def latest(self, path, stop=0): 144 def latest(self, path, stop=0):
146 'find the latest revision affecting path, up to stop' 145 'find the latest revision affecting path, up to stop'
147 if not stop: 146 if not stop:
180 def reparent(self, module): 179 def reparent(self, module):
181 svn_url = self.base + module 180 svn_url = self.base + module
182 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding)) 181 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
183 svn.ra.reparent(self.ra, svn_url.encode(self.encoding)) 182 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
184 183
185 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347, module=None): 184 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
186 def get_entry_from_path(path, module=self.module): 185 def get_entry_from_path(path, module=self.module):
187 # Given the repository url of this wc, say 186 # Given the repository url of this wc, say
188 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" 187 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
189 # extract the "entry" portion (a relative path) from what 188 # extract the "entry" portion (a relative path) from what
190 # svn log --xml says, ie 189 # svn log --xml says, ie
209 208
210 if self.is_blacklisted(revnum): 209 if self.is_blacklisted(revnum):
211 self.ui.note('skipping blacklisted revision %d\n' % revnum) 210 self.ui.note('skipping blacklisted revision %d\n' % revnum)
212 return 211 return
213 212
214 self.ui.note("parsing revision %d\n" % revnum) 213 self.ui.debug("parsing revision %d\n" % revnum)
215 214
216 if orig_paths is None: 215 if orig_paths is None:
217 self.ui.debug('revision %d has no entries\n' % revnum) 216 self.ui.debug('revision %d has no entries\n' % revnum)
218 return 217 return
219 218
240 if path == self.module: # Follow branching back in history 239 if path == self.module: # Follow branching back in history
241 ent = orig_paths[path] 240 ent = orig_paths[path]
242 if ent: 241 if ent:
243 if ent.copyfrom_path: 242 if ent.copyfrom_path:
244 # ent.copyfrom_rev may not be the actual last revision 243 # ent.copyfrom_rev may not be the actual last revision
245 prev = self.latest(ent.copyfrom_path, revnum) 244 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
246 self.modulemap[prev] = ent.copyfrom_path 245 self.modulemap[prev] = ent.copyfrom_path
247 parents = [self.rev(prev, ent.copyfrom_path)] 246 parents = [self.rev(prev, ent.copyfrom_path)]
247 self.ui.note('found parent of branch %s at %d: %s\n' % \
248 (self.module, prev, ent.copyfrom_path))
248 else: 249 else:
249 self.ui.debug("No copyfrom path, don't know what to do.\n") 250 self.ui.debug("No copyfrom path, don't know what to do.\n")
250 # Maybe it was added and there is no more history. 251 # Maybe it was added and there is no more history.
251 entrypath = get_entry_from_path(path, module=self.module) 252 entrypath = get_entry_from_path(path, module=self.module)
252 # self.ui.write("entrypath %s\n" % entrypath) 253 # self.ui.write("entrypath %s\n" % entrypath)
271 copies[self.recode(entry)] = self.recode(copyfrom_path) 272 copies[self.recode(entry)] = self.recode(copyfrom_path)
272 entries.append(self.recode(entry)) 273 entries.append(self.recode(entry))
273 elif kind == 0: # gone, but had better be a deleted *file* 274 elif kind == 0: # gone, but had better be a deleted *file*
274 self.ui.debug("gone from %s\n" % ent.copyfrom_rev) 275 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
275 276
276 fromrev = revnum - 1 277 # if a branch is created but entries are removed in the same
277 # might always need to be revnum - 1 in these 3 lines? 278 # changeset, get the right fromrev
278 old_module = self.modulemap.get(fromrev, self.module) 279 if parents:
280 uuid, old_module, fromrev = self.revsplit(parents[0])
281 else:
282 fromrev = revnum - 1
283 # might always need to be revnum - 1 in these 3 lines?
284 old_module = self.modulemap.get(fromrev, self.module)
285
279 basepath = old_module + "/" + get_entry_from_path(path, module=self.module) 286 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
280 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module) 287 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
281 288
282 def lookup_parts(p): 289 def lookup_parts(p):
283 rc = None 290 rc = None
284 parts = p.split("/") 291 parts = p.split("/")
285 for i in range(len(parts)): 292 for i in range(len(parts)):
286 part = "/".join(parts[:i]) 293 part = "/".join(parts[:i])
287 info = part, copyfrom.get(part, None) 294 info = part, copyfrom.get(part, None)
288 if info[1] is not None: 295 if info[1] is not None:
289 self.ui.debug("Found parent directory %s\n" % info) 296 self.ui.debug("Found parent directory %s\n" % info[1])
290 rc = info 297 rc = info
291 return rc 298 return rc
292 299
293 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath)) 300 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
294 301
425 self.commits[rev] = cset 432 self.commits[rev] = cset
426 if self.child_cset and not self.child_cset.parents: 433 if self.child_cset and not self.child_cset.parents:
427 self.child_cset.parents = [rev] 434 self.child_cset.parents = [rev]
428 self.child_cset = cset 435 self.child_cset = cset
429 436
430 self.ui.note('fetching revision log from %d to %d\n' % \ 437 self.ui.note('fetching revision log for "%s" from %d to %d\n' % \
431 (from_revnum, to_revnum)) 438 (self.module, from_revnum, to_revnum))
432 439
433 if module is None:
434 module = self.module
435 try: 440 try:
436 discover_changed_paths = True 441 discover_changed_paths = True
437 strict_node_history = False 442 strict_node_history = False
438 svn.ra.get_log(self.ra, [module], from_revnum, to_revnum, 0, 443 svn.ra.get_log(self.ra, [self.module], from_revnum, to_revnum, 0,
439 discover_changed_paths, strict_node_history, 444 discover_changed_paths, strict_node_history,
440 parselogentry) 445 parselogentry)
441 self.last_revnum = to_revnum 446 self.last_revnum = to_revnum
442 except SubversionException, (_, num): 447 except SubversionException, (_, num):
443 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION: 448 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
444 raise NoSuchRevision(branch=self, 449 raise NoSuchRevision(branch=self,
445 revision="Revision number %d" % to_revnum) 450 revision="Revision number %d" % to_revnum)
446 raise 451 raise
447 452
448 def getheads(self): 453 def getheads(self):
449 # svn-url@rev 454 # detect standard /branches, /tags, /trunk layout
450 # Not safe if someone committed: 455 optrev = svn.core.svn_opt_revision_t()
451 self.heads = [self.head] 456 optrev.kind = svn.core.svn_opt_revision_number
452 # print self.commits.keys() 457 optrev.value.number = self.last_changed
458 rpath = self.url.strip('/')
459 paths = svn.client.ls(rpath, optrev, False, self.ctx)
460 if 'branches' in paths and 'trunk' in paths:
461 self.module += '/trunk'
462 lt = self.latest(self.module, self.last_changed)
463 self.head = self.rev(lt)
464 self.heads = [self.head]
465 branches = svn.client.ls(rpath + '/branches', optrev, False, self.ctx)
466 for branch in branches.keys():
467 module = '/branches/' + branch
468 brevnum = self.latest(module, self.last_changed)
469 brev = self.rev(brevnum, module)
470 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
471 self.heads.append(brev)
472 else:
473 self.heads = [self.head]
453 return self.heads 474 return self.heads
454 475
455 def _getfile(self, file, rev): 476 def _getfile(self, file, rev):
456 io = StringIO() 477 io = StringIO()
457 # TODO: ra.get_file transmits the whole file instead of diffs. 478 # TODO: ra.get_file transmits the whole file instead of diffs.
495 return cl 516 return cl
496 517
497 def getcommit(self, rev): 518 def getcommit(self, rev):
498 if rev not in self.commits: 519 if rev not in self.commits:
499 uuid, module, revnum = self.revsplit(rev) 520 uuid, module, revnum = self.revsplit(rev)
500 minrev = revnum - LOG_BATCH_SIZE > 0 and revnum - LOG_BATCH_SIZE or 0 521 self.module = module
501 self._fetch_revisions(from_revnum=revnum, to_revnum=0, 522 self.reparent(module)
502 module=module) 523 self._fetch_revisions(from_revnum=revnum, to_revnum=0)
503 return self.commits[rev] 524 return self.commits[rev]
504 525
505 def gettags(self): 526 def gettags(self):
506 tags = {} 527 tags = {}
507 def parselogentry(*arg, **args): 528 def parselogentry(*arg, **args):
526 def _find_children(self, path, revnum): 547 def _find_children(self, path, revnum):
527 path = path.strip("/") 548 path = path.strip("/")
528 549
529 def _find_children_fallback(path, revnum): 550 def _find_children_fallback(path, revnum):
530 # SWIG python bindings for getdir are broken up to at least 1.4.3 551 # SWIG python bindings for getdir are broken up to at least 1.4.3
531 if not hasattr(self, 'client_ctx'):
532 self.client_ctx = svn.client.create_context()
533 pool = Pool() 552 pool = Pool()
534 optrev = svn.core.svn_opt_revision_t() 553 optrev = svn.core.svn_opt_revision_t()
535 optrev.kind = svn.core.svn_opt_revision_number 554 optrev.kind = svn.core.svn_opt_revision_number
536 optrev.value.number = revnum 555 optrev.value.number = revnum
537 rpath = '/'.join([self.base, path]).strip('/') 556 rpath = '/'.join([self.base, path]).strip('/')
538 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.client_ctx, pool).keys()] 557 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.ctx, pool).keys()]
539 558
540 if hasattr(self, '_find_children_fallback'): 559 if hasattr(self, '_find_children_fallback'):
541 return _find_children_fallback(path, revnum) 560 return _find_children_fallback(path, revnum)
542 561
543 self.reparent("/" + path) 562 self.reparent("/" + path)