view hgext/convert/hg.py @ 16719:e7bf09acd410

localrepo: add branchtip() method for faster single-branch lookups For the PyPy repo with 744 branches and 843 branch heads, this brings hg log -r default over NFS from: CallCount Recursive Total(ms) Inline(ms) module:lineno(function) 3249 0 1.3222 1.3222 <open> 3244 0 0.6211 0.6211 <method 'close' of 'file' objects> 3243 0 0.0800 0.0800 <method 'read' of 'file' objects> 3241 0 0.0660 0.0660 <method 'seek' of 'file' objects> 3905 0 0.0476 0.0476 <zlib.decompress> 3281 0 2.6756 0.0472 mercurial.changelog:182(read) +3281 0 2.5256 0.0453 +mercurial.revlog:881(revision) +3276 0 0.0389 0.0196 +mercurial.changelog:28(decodeextra) +6562 0 0.0123 0.0123 +<method 'split' of 'str' objects> +6562 0 0.0408 0.0073 +mercurial.encoding:61(tolocal) +3281 0 0.0054 0.0054 +<method 'index' of 'str' objects> 3241 0 2.2464 0.0456 mercurial.revlog:818(_loadchunk) +3241 0 0.6205 0.6205 +<method 'close' of 'file' objects> +3241 0 0.0765 0.0765 +<method 'read' of 'file' objects> +3241 0 0.0660 0.0660 +<method 'seek' of 'file' objects> +3241 0 1.4209 0.0135 +mercurial.store:374(__call__) +3241 0 0.0122 0.0107 +mercurial.revlog:810(_addchunk) 3281 0 2.5256 0.0453 mercurial.revlog:881(revision) +3280 0 0.0175 0.0175 +mercurial.revlog:305(rev) +3281 0 2.2819 0.0119 +mercurial.revlog:847(_chunkraw) +3281 0 0.0603 0.0083 +mercurial.revlog:945(_checkhash) +3281 0 0.0051 0.0051 +mercurial.revlog:349(flags) +3281 0 0.0040 0.0040 +<mercurial.mpatch.patches> 13682 0 0.0479 0.0248 <method 'decode' of 'str' objects> +7418 0 0.0228 0.0076 +encodings.utf_8:15(decode) +1 0 0.0003 0.0000 +encodings:71(search_function) 3248 0 1.3995 0.0246 mercurial.scmutil:218(__call__) +3248 0 1.3222 1.3222 +<open> +3248 0 0.0235 0.0184 +os.path:80(split) +3248 0 0.0084 0.0068 +mercurial.scmutil:92(__call__) Time: real 2.750 secs (user 0.680+0.000 sys 0.360+0.000) down to: CallCount Recursive Total(ms) Inline(ms) module:lineno(function) 55 31 0.0197 0.0163 <__import__> +1 0 0.0006 0.0002 +mercurial.context:8(<module>) +1 0 0.0042 0.0001 +mercurial.revlog:12(<module>) +1 0 0.0002 0.0001 +mercurial.match:8(<module>) +1 0 0.0003 0.0001 +mercurial.dirstate:7(<module>) +1 0 0.0057 0.0001 +mercurial.changelog:8(<module>) 1 0 0.0117 0.0032 mercurial.localrepo:525(_readbranchcache) +844 0 0.0015 0.0015 +<binascii.unhexlify> +845 0 0.0010 0.0010 +<method 'split' of 'str' objects> +843 0 0.0045 0.0009 +mercurial.encoding:61(tolocal) +843 0 0.0004 0.0004 +<method 'setdefault' of 'dict' objects> +1 0 0.0003 0.0003 +<method 'close' of 'file' objects> 3 0 0.0029 0.0029 <method 'read' of 'file' objects> 9 0 0.0018 0.0018 <open> 990 0 0.0017 0.0017 <binascii.unhexlify> 53 0 0.0016 0.0016 mercurial.demandimport:43(__init__) 862 0 0.0015 0.0015 <_codecs.utf_8_decode> 862 0 0.0037 0.0014 <method 'decode' of 'str' objects> +862 0 0.0023 0.0008 +encodings.utf_8:15(decode) 981 0 0.0011 0.0011 <method 'split' of 'str' objects> 861 0 0.0046 0.0009 mercurial.encoding:61(tolocal) +861 0 0.0037 0.0014 +<method 'decode' of 'str' objects> 862 0 0.0023 0.0008 encodings.utf_8:15(decode) +862 0 0.0015 0.0015 +<_codecs.utf_8_decode> 4 0 0.0008 0.0008 <method 'close' of 'file' objects> 179 154 0.0202 0.0004 mercurial.demandimport:83(__getattribute__) +36 11 0.0199 0.0003 +mercurial.demandimport:55(_load) +72 0 0.0001 0.0001 +mercurial.demandimport:83(__getattribute__) +36 0 0.0000 0.0000 +<getattr> 1 0 0.0015 0.0004 mercurial.tags:148(_readtagcache) Time: real 0.060 secs (user 0.030+0.000 sys 0.010+0.000)
author Brodie Rao <brodie@sf.io>
date Sun, 13 May 2012 14:04:04 +0200
parents f366d4c2ff34
children 1093ad1e8903
line wrap: on
line source

# 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

from common import NoRepo, commit, converter_source, converter_sink

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 getheads(self):
        h = self.repo.changelog.heads()
        return [hex(x) for x in h]

    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 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(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
        extra = commit.extra.copy()
        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

        try:
            oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
        except Exception:
            oldlines = []

        newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
        if newlines == oldlines:
            return None, None
        data = "".join(newlines)
        def getfilectx(repo, memctx, f):
            return context.memfilectx(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"))
        for bookmark in updatedbookmark:
            self.repo._bookmarks[bookmark] = bin(updatedbookmark[bookmark])
            bookmarks.write(self.repo)

    def hascommit(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')
        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 rev in self.repo.changelog.descendants(startrev):
                children[self.repo.changelog.node(rev)] = 1
            self.keep = children.__contains__
        else:
            self.keep = util.always

    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):
        if self.rev:
            heads = [self.repo[self.rev].node()]
        else:
            heads = self.repo.heads()
        return [hex(h) for h in 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 or not self.keep(copynode):
                    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):
        tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
        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 lookuprev(self, rev):
        try:
            return hex(self.repo.lookup(rev))
        except error.RepoError:
            return None

    def getbookmarks(self):
        return bookmarks.listbookmarks(self.repo)