hgext3rd/topic/topicmap.py
changeset 1901 85390446f8c1
parent 1890 e846b8f402d0
child 1927 cf33ba7fbf4b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/topicmap.py	Thu Mar 17 09:12:18 2016 -0700
@@ -0,0 +1,200 @@
+from mercurial import branchmap
+from mercurial import encoding
+from mercurial import error
+from mercurial import scmutil
+from mercurial import util
+from mercurial.node import hex, bin, nullid
+
+def _filename(repo):
+    """name of a branchcache file for a given repo or repoview"""
+    filename = "cache/topicmap"
+    if repo.filtername:
+        filename = '%s-%s' % (filename, repo.filtername)
+    return filename
+
+oldbranchcache = branchmap.branchcache
+
+def _phaseshash(repo, maxrev):
+    revs = set()
+    cl = repo.changelog
+    fr = cl.filteredrevs
+    nm = cl.nodemap
+    for roots in repo._phasecache.phaseroots[1:]:
+        for n in roots:
+            r = nm.get(n)
+            if r not in fr and r < maxrev:
+                revs.add(r)
+    key = nullid
+    revs = sorted(revs)
+    if revs:
+        s = util.sha1()
+        for rev in revs:
+            s.update('%s;' % rev)
+        key = s.digest()
+    return key
+
+class topiccache(oldbranchcache):
+
+    def __init__(self, *args, **kwargs):
+        otherbranchcache = branchmap.branchcache
+        try:
+            # super() call may fail otherwise
+            branchmap.branchcache = oldbranchcache
+            super(topiccache, self).__init__(*args, **kwargs)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = nullid
+        finally:
+            branchmap.branchcache = otherbranchcache
+
+    def copy(self):
+        """return an deep copy of the branchcache object"""
+        new =  topiccache(self, self.tipnode, self.tiprev, self.filteredhash,
+                          self._closednodes)
+        if self.filteredhash is None:
+            self.filteredhash = nullid
+        new.phaseshash = self.phaseshash
+        return new
+
+    def branchtip(self, branch, topic=''):
+        '''Return the tipmost open head on branch head, otherwise return the
+        tipmost closed head on branch.
+        Raise KeyError for unknown branch.'''
+        if topic:
+            branch = '%s:%s' % (branch, topic)
+        return super(topiccache, self).branchtip(branch)
+
+    def branchheads(self, branch, closed=False, topic=''):
+        if topic:
+            branch = '%s:%s' % (branch, topic)
+        return super(topiccache, self).branchheads(branch, closed=closed)
+
+    def validfor(self, repo):
+        """Is the cache content valid regarding a repo
+
+        - False when cached tipnode is unknown or if we detect a strip.
+        - True when cache is up to date or a subset of current repo."""
+        # This is copy paste of mercurial.branchmap.branchcache.validfor in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            if (self.tipnode == repo.changelog.node(self.tiprev)):
+                fh = scmutil.filteredhash(repo, self.tiprev)
+                if fh is None:
+                    fh = nullid
+                if ((self.filteredhash == fh)
+                     and (self.phaseshash == _phaseshash(repo, self.tiprev))):
+                    return True
+            return False
+        except IndexError:
+            return False
+
+    def write(self, repo):
+        # This is copy paste of mercurial.branchmap.branchcache.write in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            f = repo.vfs(_filename(repo), "w", atomictemp=True)
+            cachekey = [hex(self.tipnode), str(self.tiprev)]
+            # [CHANGE] we need a hash in all cases
+            assert self.filteredhash is not None
+            cachekey.append(hex(self.filteredhash))
+            cachekey.append(hex(self.phaseshash))
+            f.write(" ".join(cachekey) + '\n')
+            nodecount = 0
+            for label, nodes in sorted(self.iteritems()):
+                for node in nodes:
+                    nodecount += 1
+                    if node in self._closednodes:
+                        state = 'c'
+                    else:
+                        state = 'o'
+                    f.write("%s %s %s\n" % (hex(node), state,
+                                            encoding.fromlocal(label)))
+            f.close()
+            repo.ui.log('branchcache',
+                        'wrote %s branch cache with %d labels and %d nodes\n',
+                        repo.filtername, len(self), nodecount)
+        except (IOError, OSError, error.Abort) as inst:
+            repo.ui.debug("couldn't write branch cache: %s\n" % inst)
+            # Abort may be raise by read only opener
+            pass
+
+    def update(self, repo, revgen):
+        """Given a branchhead cache, self, that may have extra nodes or be
+        missing heads, and a generator of nodes that are strictly a superset of
+        heads missing, this function updates self to be correct.
+        """
+        oldgetbranchinfo = repo.revbranchcache().branchinfo
+        try:
+            def branchinfo(r):
+                info = oldgetbranchinfo(r)
+                topic = ''
+                ctx = repo[r]
+                if ctx.mutable():
+                    topic = ctx.topic()
+                branch = info[0]
+                if topic:
+                    branch = '%s:%s' % (branch, topic)
+                return (branch, info[1])
+            repo.revbranchcache().branchinfo = branchinfo
+            super(topiccache, self).update(repo, revgen)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = _phaseshash(repo, self.tiprev)
+        finally:
+            repo.revbranchcache().branchinfo = oldgetbranchinfo
+
+def readtopicmap(repo):
+    # This is copy paste of mercurial.branchmap.read in 69077c65919d
+    # With a small changes to the cache key handling to include phase
+    # information that impact the topic cache.
+    #
+    # All code changes should be flagged on site.
+    try:
+        f = repo.vfs(_filename(repo))
+        lines = f.read().split('\n')
+        f.close()
+    except (IOError, OSError):
+        return None
+
+    try:
+        cachekey = lines.pop(0).split(" ", 2)
+        last, lrev = cachekey[:2]
+        last, lrev = bin(last), int(lrev)
+        filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash
+        partial = branchcache(tipnode=last, tiprev=lrev,
+                              filteredhash=filteredhash)
+        partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash
+        if not partial.validfor(repo):
+            # invalidate the cache
+            raise ValueError('tip differs')
+        cl = repo.changelog
+        for l in lines:
+            if not l:
+                continue
+            node, state, label = l.split(" ", 2)
+            if state not in 'oc':
+                raise ValueError('invalid branch state')
+            label = encoding.tolocal(label.strip())
+            node = bin(node)
+            if not cl.hasnode(node):
+                raise ValueError('node %s does not exist' % hex(node))
+            partial.setdefault(label, []).append(node)
+            if state == 'c':
+                partial._closednodes.add(node)
+    except KeyboardInterrupt:
+        raise
+    except Exception as inst:
+        if repo.ui.debugflag:
+            msg = 'invalid branchheads cache'
+            if repo.filtername is not None:
+                msg += ' (%s)' % repo.filtername
+            msg += ': %s\n'
+            repo.ui.debug(msg % inst)
+        partial = None
+    return partial