view hggit/overlay.py @ 1001:fa23c454c528

manifest: update to work with new manifest classes in core Core hg has refactored the manifest logic to remove the manifest class. So we need to update our use of manifests to use manifestlog and manifestctx instead. Tested this by running the tests against hg 3.8, 3.9, 4.0, and @ (c27614f2dec).
author Durham Goode <durham@fb.com>
date Thu, 17 Nov 2016 16:22:42 -0800
parents 7b11e6348810
children 6faa33912cd5
line wrap: on
line source

# overlay classes for repositories
# unifies access to unimported git objects and committed hg objects
# designed to support incoming
#
# incomplete, implemented on demand

from mercurial import (
    ancestor,
    changelog,
    manifest,
    context,
    util,
)
from mercurial.node import bin, hex, nullid

def _maybehex(n):
    if len(n) == 20:
        return hex(n)
    return n

class overlaymanifest(object):
    def __init__(self, repo, sha):
        self.repo = repo
        self.tree = repo.handler.git.get_object(sha)
        self._map = None
        self._flags = None

    def withflags(self):
        self.load()
        return set([path for path, flag in self._flags.iteritems()
                    if flag != ''])

    def copy(self):
        return overlaymanifest(self.repo, self.tree.id)

    def keys(self):
        self.load()
        return self._map.keys()

    def iterkeys(self):
        return iter(self.keys())

    def load(self):
        if self._map is not None:
            return

        self._map = {}
        self._flags = {}

        def hgflag(gitflag):
            if gitflag & 0100:
                return 'x'
            elif gitflag & 020000:
                return 'l'
            else:
                return ''

        def addtree(tree, dirname):
            for entry in tree.iteritems():
                if entry.mode & 040000:
                    # expand directory
                    subtree = self.repo.handler.git.get_object(entry.sha)
                    addtree(subtree, dirname + entry.path + '/')
                else:
                    path = dirname + entry.path
                    self._map[path] = bin(entry.sha)
                    self._flags[path] = hgflag(entry.mode)

        addtree(self.tree, '')

    def matches(self, match):
        '''generate a new manifest filtered by the match argument'''
        if match.always():
            return self.copy()

        mf = self.copy()
        for fn in mf.keys():
            if not match(fn):
                del mf[fn]
        return mf

    def iteritems(self):
        self.load()
        return self._map.iteritems()

    def __iter__(self):
        self.load()
        return self._map.__iter__()

    def __getitem__(self, path):
        self.load()
        return self._map[path]

    def __contains__(self, path):
        self.load()
        return path in self._map

    def get(self, path, default=None):
        self.load()
        return self._map.get(path, default)

    def diff(self, m2, clean=False):
        self.load()
        if isinstance(m2, overlaymanifest):
            m2.load()

        # below code copied from manifest.py:manifestdict.diff
        diff = {}

        try:
            m2flagget = m2.flags
        except AttributeError:
            # Mercurial <= 3.3
            m2flagget = m2._flags.get

        for fn, n1 in self.iteritems():
            fl1 = self._flags.get(fn, '')
            n2 = m2.get(fn, None)
            fl2 = m2flagget(fn, '')
            if n2 is None:
                fl2 = ''
            if n1 != n2 or fl1 != fl2:
                diff[fn] = ((n1, fl1), (n2, fl2))
            elif clean:
                diff[fn] = None

        for fn, n2 in m2.iteritems():
            if fn not in self:
                fl2 = m2flagget(fn, '')
                diff[fn] = ((None, ''), (n2, fl2))

        return diff

    def __delitem__(self, path):
        del self._map[path]

def wrapmanifestdictdiff(orig, self, m2, clean=False):
    '''avoid calling into lazymanifest code if m2 is an overlaymanifest'''
    if isinstance(m2, overlaymanifest):
        diff = m2.diff(self, clean=clean)
        # since we calculated the diff with m2 vs m1, flip it around
        for fn in diff:
            c1, c2 = diff[fn]
            diff[fn] = c2, c1
        return diff
    else:
        return orig(self, m2, clean=clean)

class overlayfilectx(object):
    def __init__(self, repo, path, fileid=None):
        self.repo = repo
        self._path = path
        self.fileid = fileid

    # this is a hack to skip copy detection
    def ancestors(self):
        return [self, self]

    def filenode(self):
        return nullid

    def rev(self):
        return -1

    def path(self):
        return self._path

    def filelog(self):
        return self.fileid

    def data(self):
        blob = self.repo.handler.git.get_object(_maybehex(self.fileid))
        return blob.data

class overlaychangectx(context.changectx):
    def __init__(self, repo, sha):
        self.repo = repo
        if not isinstance(sha, basestring):
            sha = sha.hex()
        self.commit = repo.handler.git.get_object(_maybehex(sha))
        self._overlay = getattr(repo, 'gitoverlay', repo)
        self._rev = self._overlay.rev(bin(self.commit.id))

    def node(self):
        return bin(self.commit.id)

    def rev(self):
        return self._rev

    def date(self):
        return self.commit.author_time, self.commit.author_timezone

    def branch(self):
        return 'default'

    def user(self):
        return self.commit.author

    def files(self):
        return []

    def extra(self):
        return {}

    def description(self):
        return self.commit.message

    def parents(self):
        cl = self.repo.changelog
        parents = cl.parents(cl.node(self._rev))
        if not parents:
            return [self.repo['null']]
        if parents[1] == nullid:
            parents = parents[:-1]
        return [self.repo[sha] for sha in parents]

    def manifestnode(self):
        return bin(self.commit.tree)

    def hex(self):
        return self.commit.id

    def tags(self):
        return []

    def bookmarks(self):
        return []

    def manifest(self):
        return overlaymanifest(self._overlay, self.commit.tree)

    def filectx(self, path, filelog=None):
        mf = self.manifest()
        return overlayfilectx(self._overlay, path, mf[path])

    def flags(self, path):
        mf = self.manifest()
        return mf.flags(path)

    def __nonzero__(self):
        return True

    def phase(self):
        try:
            from mercurial import phases
            return phases.draft
        except (AttributeError, ImportError):
            return 1

    def totuple(self):
        return (self.commit.tree, self.user(), self.date(), self.files(),
                self.description(), self.extra())

class overlayrevlog(object):
    def __init__(self, repo, base):
        self.repo = repo
        self.base = base

    def parents(self, n):
        gitrev = self.repo.revmap.get(n)
        if gitrev is None:
            # we've reached a revision we have
            return self.base.parents(n)
        commit = self.repo.handler.git.get_object(_maybehex(n))

        if not commit.parents:
            return [nullid, nullid]

        def gitorhg(n):
            hn = self.repo.handler.map_hg_get(hex(n))
            if hn is not None:
                return bin(hn)
            return n

        # currently ignores the octopus
        p1 = gitorhg(bin(commit.parents[0]))
        if len(commit.parents) > 1:
            p2 = gitorhg(bin(commit.parents[1]))
        else:
            p2 = nullid

        return [p1, p2]

    def ancestor(self, a, b):
        anode = self.repo.nodemap.get(a)
        bnode = self.repo.nodemap.get(b)
        if anode is None and bnode is None:
            return self.base.ancestor(a, b)
        ancs = ancestor.ancestors(self.parentrevs, a, b)
        if ancs:
            return min(map(self.node, ancs))
        return nullid

    def parentrevs(self, rev):
        return [self.rev(p) for p in self.parents(self.node(rev))]

    def node(self, rev):
        gitnode = self.repo.nodemap.get(rev)
        if gitnode is None:
            return self.base.node(rev)
        return gitnode

    def rev(self, n):
        gitrev = self.repo.revmap.get(n)
        if gitrev is None:
            return self.base.rev(n)
        return gitrev

    def __len__(self):
        return len(self.repo.handler.repo) + len(self.repo.revmap)

class overlayoldmanifestlog(overlayrevlog):
    def read(self, sha):
        if sha == nullid:
            return manifest.manifestdict()
        return overlaymanifest(self.repo, sha)

    def __getitem__(self, sha):
        return overlaymanifestctx(self.repo, sha)

class overlaymanifestrevlog(overlayrevlog):
    pass

class overlaymanifestctx(object):
    def __init__(self, repo, node):
        self._repo = repo
        self._node = node

    def read(self):
        return overlaymanifest(self._repo, self._node)

try:
    class overlaymanifestlog(manifest.manifestlog):
        def __init__(self, repo):
            self._repo = repo

        # Needed for 4.0, since __getitem__ did not redirect to get() in that
        # release.
        def __getitem__(self, node):
            return self.get('', node)

        def get(self, dir, node):
            if dir:
                raise RuntimeError("hggit doesn't support treemanifests")
            if node == nullid:
                return manifest.manifestctx()
            return overlaymanifestctx(self._repo, node)
except AttributeError:
    # manifestlog did not exist prior to 4.0
    pass

class overlaychangelog(overlayrevlog):
    def read(self, sha):
        if isinstance(sha, int):
            sha = self.node(sha)
        if sha == nullid:
            return (nullid, "", (0, 0), [], "", {})
        try:
            return self.base.read(sha)
        except LookupError:
            return overlaychangectx(self.repo, sha).totuple()

    def changelogrevision(self, noderev):
        return changelog._changelogrevision(*self.read(noderev))

class overlayrepo(object):
    def __init__(self, handler, commits, refs):
        self.handler = handler

        self.changelog = overlaychangelog(self, handler.repo.changelog)
        if util.safehasattr(handler.repo, 'manifest'):
            self.manifest = overlayoldmanifestlog(self, handler.repo.manifest)
            # new as of mercurial 3.9+
            self.manifestlog = self.manifest
        else:
            # no more manifest class as of 4.1
            self.manifestlog = overlaymanifestlog(self)

        # for incoming -p
        self.root = handler.repo.root
        self.getcwd = handler.repo.getcwd
        # self.status = handler.repo.status
        self.ui = handler.repo.ui

        self.revmap = None
        self.nodemap = None
        self.refmap = None
        self.tagmap = None

        self._makemaps(commits, refs)

        try:
            # Mercurial >= 3.3
            from mercurial import namespaces
            self.names = namespaces.namespaces()
        except (AttributeError, ImportError):
            pass

    def _constructmanifest(self):
        return overlaymanifestrevlog(self,
                self.handler.repo._constructmanifest())

    def __getitem__(self, n):
        if n not in self.revmap:
            return self.handler.repo[n]
        return overlaychangectx(self, n)

    def _handlerhack(self, method, *args, **kwargs):
        nothing = object()
        r = self.handler.repo
        oldhandler = getattr(r, 'handler', nothing)
        oldoverlay = getattr(r, 'gitoverlay', nothing)
        r.handler = self.handler
        r.gitoverlay = self
        try:
            return getattr(r, method)(*args, **kwargs)
        finally:
            if oldhandler is nothing:
                del r.handler
            else:
                r.handler = oldhandler
            if oldoverlay is nothing:
                del r.gitoverlay
            else:
                r.gitoverlay = oldoverlay

    def status(self, *args, **kwargs):
        return self._handlerhack('status', *args, **kwargs)

    def node(self, n):
        """Returns an Hg or Git hash for the specified Git hash"""
        if bin(n) in self.revmap:
            return n
        return self.handler.map_hg_get(n)

    def nodebookmarks(self, n):
        return self.refmap.get(n, [])

    def nodetags(self, n):
        return self.tagmap.get(n, [])

    def rev(self, n):
        return self.revmap[n]

    def filectx(self, path, fileid=None):
        return overlayfilectx(self, path, fileid=fileid)

    def _makemaps(self, commits, refs):
        baserev = self.handler.repo['tip'].rev()
        self.revmap = {}
        self.nodemap = {}
        for i, n in enumerate(commits):
            rev = baserev + i + 1
            self.revmap[n] = rev
            self.nodemap[rev] = n

        self.refmap = {}
        self.tagmap = {}
        for ref in refs:
            if ref.startswith('refs/heads/'):
                refname = ref[11:]
                self.refmap.setdefault(bin(refs[ref]), []).append(refname)
            elif ref.startswith('refs/tags/'):
                tagname = ref[10:]
                self.tagmap.setdefault(bin(refs[ref]), []).append(tagname)