/hgext/convert/hg.py
Python | 470 lines | 412 code | 37 blank | 21 comment | 65 complexity | 7af81c935711d01a1f1e53abb44ddf1e MD5 | raw file
Possible License(s): GPL-2.0
- # hg.py - hg backend for convert extension
- #
- # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- # Notes for hg->hg conversion:
- #
- # * Old versions of Mercurial didn't trim the whitespace from the ends
- # of commit messages, but new versions do. Changesets created by
- # those older versions, then converted, may thus have different
- # hashes for changesets that are otherwise identical.
- #
- # * Using "--config convert.hg.saverev=true" will make the source
- # identifier to be stored in the converted revision. This will cause
- # the converted revision to have a different identity than the
- # source.
- import os, time, cStringIO
- from mercurial.i18n import _
- from mercurial.node import bin, hex, nullid
- from mercurial import hg, util, context, bookmarks, error, scmutil
- from common import NoRepo, commit, converter_source, converter_sink
- import re
- sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
- class mercurial_sink(converter_sink):
- def __init__(self, ui, path):
- converter_sink.__init__(self, ui, path)
- self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
- self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
- self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
- self.lastbranch = None
- if os.path.isdir(path) and len(os.listdir(path)) > 0:
- try:
- self.repo = hg.repository(self.ui, path)
- if not self.repo.local():
- raise NoRepo(_('%s is not a local Mercurial repository')
- % path)
- except error.RepoError, err:
- ui.traceback()
- raise NoRepo(err.args[0])
- else:
- try:
- ui.status(_('initializing destination %s repository\n') % path)
- self.repo = hg.repository(self.ui, path, create=True)
- if not self.repo.local():
- raise NoRepo(_('%s is not a local Mercurial repository')
- % path)
- self.created.append(path)
- except error.RepoError:
- ui.traceback()
- raise NoRepo(_("could not create hg repository %s as sink")
- % path)
- self.lock = None
- self.wlock = None
- self.filemapmode = False
- def before(self):
- self.ui.debug('run hg sink pre-conversion action\n')
- self.wlock = self.repo.wlock()
- self.lock = self.repo.lock()
- def after(self):
- self.ui.debug('run hg sink post-conversion action\n')
- if self.lock:
- self.lock.release()
- if self.wlock:
- self.wlock.release()
- def revmapfile(self):
- return self.repo.join("shamap")
- def authorfile(self):
- return self.repo.join("authormap")
- def setbranch(self, branch, pbranches):
- if not self.clonebranches:
- return
- setbranch = (branch != self.lastbranch)
- self.lastbranch = branch
- if not branch:
- branch = 'default'
- pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
- pbranch = pbranches and pbranches[0][1] or 'default'
- branchpath = os.path.join(self.path, branch)
- if setbranch:
- self.after()
- try:
- self.repo = hg.repository(self.ui, branchpath)
- except Exception:
- self.repo = hg.repository(self.ui, branchpath, create=True)
- self.before()
- # pbranches may bring revisions from other branches (merge parents)
- # Make sure we have them, or pull them.
- missings = {}
- for b in pbranches:
- try:
- self.repo.lookup(b[0])
- except Exception:
- missings.setdefault(b[1], []).append(b[0])
- if missings:
- self.after()
- for pbranch, heads in sorted(missings.iteritems()):
- pbranchpath = os.path.join(self.path, pbranch)
- prepo = hg.peer(self.ui, {}, pbranchpath)
- self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
- self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
- self.before()
- def _rewritetags(self, source, revmap, data):
- fp = cStringIO.StringIO()
- for line in data.splitlines():
- s = line.split(' ', 1)
- if len(s) != 2:
- continue
- revid = revmap.get(source.lookuprev(s[0]))
- if not revid:
- continue
- fp.write('%s %s\n' % (revid, s[1]))
- return fp.getvalue()
- def putcommit(self, files, copies, parents, commit, source, revmap):
- files = dict(files)
- def getfilectx(repo, memctx, f):
- v = files[f]
- data, mode = source.getfile(f, v)
- if f == '.hgtags':
- data = self._rewritetags(source, revmap, data)
- return context.memfilectx(self.repo, f, data, 'l' in mode,
- 'x' in mode, copies.get(f))
- pl = []
- for p in parents:
- if p not in pl:
- pl.append(p)
- parents = pl
- nparents = len(parents)
- if self.filemapmode and nparents == 1:
- m1node = self.repo.changelog.read(bin(parents[0]))[0]
- parent = parents[0]
- if len(parents) < 2:
- parents.append(nullid)
- if len(parents) < 2:
- parents.append(nullid)
- p2 = parents.pop(0)
- text = commit.desc
- sha1s = re.findall(sha1re, text)
- for sha1 in sha1s:
- oldrev = source.lookuprev(sha1)
- newrev = revmap.get(oldrev)
- if newrev is not None:
- text = text.replace(sha1, newrev[:len(sha1)])
- extra = commit.extra.copy()
- for label in ('source', 'transplant_source', 'rebase_source'):
- node = extra.get(label)
- if node is None:
- continue
- # Only transplant stores its reference in binary
- if label == 'transplant_source':
- node = hex(node)
- newrev = revmap.get(node)
- if newrev is not None:
- if label == 'transplant_source':
- newrev = bin(newrev)
- extra[label] = newrev
- if self.branchnames and commit.branch:
- extra['branch'] = commit.branch
- if commit.rev:
- extra['convert_revision'] = commit.rev
- while parents:
- p1 = p2
- p2 = parents.pop(0)
- ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
- getfilectx, commit.author, commit.date, extra)
- self.repo.commitctx(ctx)
- text = "(octopus merge fixup)\n"
- p2 = hex(self.repo.changelog.tip())
- if self.filemapmode and nparents == 1:
- man = self.repo.manifest
- mnode = self.repo.changelog.read(bin(p2))[0]
- closed = 'close' in commit.extra
- if not closed and not man.cmp(m1node, man.revision(mnode)):
- self.ui.status(_("filtering out empty revision\n"))
- self.repo.rollback(force=True)
- return parent
- return p2
- def puttags(self, tags):
- try:
- parentctx = self.repo[self.tagsbranch]
- tagparent = parentctx.node()
- except error.RepoError:
- parentctx = None
- tagparent = nullid
- oldlines = set()
- for branch, heads in self.repo.branchmap().iteritems():
- for h in heads:
- if '.hgtags' in self.repo[h]:
- oldlines.update(
- set(self.repo[h]['.hgtags'].data().splitlines(True)))
- oldlines = sorted(list(oldlines))
- newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
- if newlines == oldlines:
- return None, None
- # if the old and new tags match, then there is nothing to update
- oldtags = set()
- newtags = set()
- for line in oldlines:
- s = line.strip().split(' ', 1)
- if len(s) != 2:
- continue
- oldtags.add(s[1])
- for line in newlines:
- s = line.strip().split(' ', 1)
- if len(s) != 2:
- continue
- if s[1] not in oldtags:
- newtags.add(s[1].strip())
- if not newtags:
- return None, None
- data = "".join(newlines)
- def getfilectx(repo, memctx, f):
- return context.memfilectx(repo, f, data, False, False, None)
- self.ui.status(_("updating tags\n"))
- date = "%s 0" % int(time.mktime(time.gmtime()))
- extra = {'branch': self.tagsbranch}
- ctx = context.memctx(self.repo, (tagparent, None), "update tags",
- [".hgtags"], getfilectx, "convert-repo", date,
- extra)
- self.repo.commitctx(ctx)
- return hex(self.repo.changelog.tip()), hex(tagparent)
- def setfilemapmode(self, active):
- self.filemapmode = active
- def putbookmarks(self, updatedbookmark):
- if not len(updatedbookmark):
- return
- self.ui.status(_("updating bookmarks\n"))
- destmarks = self.repo._bookmarks
- for bookmark in updatedbookmark:
- destmarks[bookmark] = bin(updatedbookmark[bookmark])
- destmarks.write()
- def hascommitfrommap(self, rev):
- # the exact semantics of clonebranches is unclear so we can't say no
- return rev in self.repo or self.clonebranches
- def hascommitforsplicemap(self, rev):
- if rev not in self.repo and self.clonebranches:
- raise util.Abort(_('revision %s not found in destination '
- 'repository (lookups with clonebranches=true '
- 'are not implemented)') % rev)
- return rev in self.repo
- class mercurial_source(converter_source):
- def __init__(self, ui, path, rev=None):
- converter_source.__init__(self, ui, path, rev)
- self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
- self.ignored = set()
- self.saverev = ui.configbool('convert', 'hg.saverev', False)
- try:
- self.repo = hg.repository(self.ui, path)
- # try to provoke an exception if this isn't really a hg
- # repo, but some other bogus compatible-looking url
- if not self.repo.local():
- raise error.RepoError
- except error.RepoError:
- ui.traceback()
- raise NoRepo(_("%s is not a local Mercurial repository") % path)
- self.lastrev = None
- self.lastctx = None
- self._changescache = None
- self.convertfp = None
- # Restrict converted revisions to startrev descendants
- startnode = ui.config('convert', 'hg.startrev')
- hgrevs = ui.config('convert', 'hg.revs')
- if hgrevs is None:
- if startnode is not None:
- try:
- startnode = self.repo.lookup(startnode)
- except error.RepoError:
- raise util.Abort(_('%s is not a valid start revision')
- % startnode)
- startrev = self.repo.changelog.rev(startnode)
- children = {startnode: 1}
- for r in self.repo.changelog.descendants([startrev]):
- children[self.repo.changelog.node(r)] = 1
- self.keep = children.__contains__
- else:
- self.keep = util.always
- if rev:
- self._heads = [self.repo[rev].node()]
- else:
- self._heads = self.repo.heads()
- else:
- if rev or startnode is not None:
- raise util.Abort(_('hg.revs cannot be combined with '
- 'hg.startrev or --rev'))
- nodes = set()
- parents = set()
- for r in scmutil.revrange(self.repo, [hgrevs]):
- ctx = self.repo[r]
- nodes.add(ctx.node())
- parents.update(p.node() for p in ctx.parents())
- self.keep = nodes.__contains__
- self._heads = nodes - parents
- def changectx(self, rev):
- if self.lastrev != rev:
- self.lastctx = self.repo[rev]
- self.lastrev = rev
- return self.lastctx
- def parents(self, ctx):
- return [p for p in ctx.parents() if p and self.keep(p.node())]
- def getheads(self):
- return [hex(h) for h in self._heads if self.keep(h)]
- def getfile(self, name, rev):
- try:
- fctx = self.changectx(rev)[name]
- return fctx.data(), fctx.flags()
- except error.LookupError, err:
- raise IOError(err)
- def getchanges(self, rev):
- ctx = self.changectx(rev)
- parents = self.parents(ctx)
- if not parents:
- files = sorted(ctx.manifest())
- # getcopies() is not needed for roots, but it is a simple way to
- # detect missing revlogs and abort on errors or populate
- # self.ignored
- self.getcopies(ctx, parents, files)
- return [(f, rev) for f in files if f not in self.ignored], {}
- if self._changescache and self._changescache[0] == rev:
- m, a, r = self._changescache[1]
- else:
- m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
- # getcopies() detects missing revlogs early, run it before
- # filtering the changes.
- copies = self.getcopies(ctx, parents, m + a)
- changes = [(name, rev) for name in m + a + r
- if name not in self.ignored]
- return sorted(changes), copies
- def getcopies(self, ctx, parents, files):
- copies = {}
- for name in files:
- if name in self.ignored:
- continue
- try:
- copysource, _copynode = ctx.filectx(name).renamed()
- if copysource in self.ignored:
- continue
- # Ignore copy sources not in parent revisions
- found = False
- for p in parents:
- if copysource in p:
- found = True
- break
- if not found:
- continue
- copies[name] = copysource
- except TypeError:
- pass
- except error.LookupError, e:
- if not self.ignoreerrors:
- raise
- self.ignored.add(name)
- self.ui.warn(_('ignoring: %s\n') % e)
- return copies
- def getcommit(self, rev):
- ctx = self.changectx(rev)
- parents = [p.hex() for p in self.parents(ctx)]
- if self.saverev:
- crev = rev
- else:
- crev = None
- return commit(author=ctx.user(),
- date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
- desc=ctx.description(), rev=crev, parents=parents,
- branch=ctx.branch(), extra=ctx.extra(),
- sortkey=ctx.rev())
- def gettags(self):
- # This will get written to .hgtags, filter non global tags out.
- tags = [t for t in self.repo.tagslist()
- if self.repo.tagtype(t[0]) == 'global']
- return dict([(name, hex(node)) for name, node in tags
- if self.keep(node)])
- def getchangedfiles(self, rev, i):
- ctx = self.changectx(rev)
- parents = self.parents(ctx)
- if not parents and i is None:
- i = 0
- changes = [], ctx.manifest().keys(), []
- else:
- i = i or 0
- changes = self.repo.status(parents[i].node(), ctx.node())[:3]
- changes = [[f for f in l if f not in self.ignored] for l in changes]
- if i == 0:
- self._changescache = (rev, changes)
- return changes[0] + changes[1] + changes[2]
- def converted(self, rev, destrev):
- if self.convertfp is None:
- self.convertfp = open(self.repo.join('shamap'), 'a')
- self.convertfp.write('%s %s\n' % (destrev, rev))
- self.convertfp.flush()
- def before(self):
- self.ui.debug('run hg source pre-conversion action\n')
- def after(self):
- self.ui.debug('run hg source post-conversion action\n')
- def hasnativeorder(self):
- return True
- def hasnativeclose(self):
- return True
- def lookuprev(self, rev):
- try:
- return hex(self.repo.lookup(rev))
- except error.RepoError:
- return None
- def getbookmarks(self):
- return bookmarks.listbookmarks(self.repo)
- def checkrevformat(self, revstr, mapname='splicemap'):
- """ Mercurial, revision string is a 40 byte hex """
- self.checkhexformat(revstr, mapname)