hgext/obsolete.py
changeset 51 d98e06ab8320
parent 50 19b22ad56b32
child 52 62bdc2567099
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/obsolete.py	Thu Sep 08 17:15:20 2011 +0200
@@ -0,0 +1,237 @@
+# obsolete.py - introduce the obsolete concept in mercurial.
+#
+# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#                Logilab SA        <contact@logilab.fr>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from mercurial import util
+from mercurial import context
+from mercurial import revset
+from mercurial import scmutil
+from mercurial import extensions
+from mercurial import pushkey
+from mercurial import discovery
+from mercurial import error
+from mercurial.node import hex, bin
+
+# Patch changectx
+#############################
+
+def obsolete(ctx):
+    """is the changeset obsolete by other"""
+    if ctx.node()is None:
+        return False
+    return bool(ctx._repo.obsoletedby(ctx.node()))
+
+context.changectx.obsolete = obsolete
+
+ohidden = context.changectx.hidden
+def hidden(ctx):
+    # hack to fill hiddenrevs
+    # compute hidden (XXX should move elsewhere)
+    if not getattr(ctx._repo.changelog, 'hiddeninit', False):
+        basicquery = 'obsolete() - (ancestors(not obsolete() or . or bookmark()))'
+        for rev in scmutil.revrange(ctx._repo, [basicquery]):
+            ctx._repo.changelog.hiddenrevs.add(rev)
+        ctx._repo.changelog.hiddeninit = True
+
+    return ohidden(ctx)
+context.changectx.hidden = hidden
+
+# revset
+#############################
+
+def revsetobsolete(repo, subset, x):
+    args = revset.getargs(x, 0, 0, 'publicheads takes no arguments')
+    return [r for r in subset if repo[r].obsolete()] # XXX slow
+
+def extsetup(ui):
+    revset.symbols["obsolete"] = revsetobsolete
+
+    def filterobsoleteout(orig, repo, remote, *args,**kwargs):
+        common, heads = orig(repo, remote, *args, **kwargs)
+
+        # filter obsolete
+        heads = set(map(repo.changelog.rev, heads))
+        obsoletes = set()
+        for obj in repo._obsobjrels:
+            try:
+                obsoletes.add(repo.changelog.rev(obj))
+            except error.LookupError:
+                pass # we don't have this node locally
+
+        outgoing = set(repo.changelog.ancestors(*heads))
+        outgoing.update(heads)
+
+        selected = outgoing - obsoletes
+        heads = sorted(map(repo.changelog.node, selected))
+
+        return common, heads
+
+    extensions.wrapfunction(discovery, 'findcommonoutgoing', filterobsoleteout)
+    
+    try:
+        rebase = extensions.find('rebase')
+        if rebase:
+            extensions.wrapfunction(rebase, 'concludenode', concludenode)
+    except KeyError:
+        pass # rebase not found
+
+# Pushkey mechanism for mutable
+#########################################
+
+def pushobsolete(repo, key, old, relations):
+    assert key == "relations"
+    w = repo.wlock()
+    try:
+        for sub, objs in relations.iteritems():
+            for obj in objs:
+                repo.addobsolete(sub, obj)
+    finally:
+        w.release()
+
+def listobsolete(repo):
+    return {'relations': repo._obssubrels}
+
+pushkey.register('obsolete', pushobsolete, listobsolete)
+
+# New commands
+#############################
+
+
+def cmddebugobsolete(ui, repo, subject, object):
+    """Add an obsolete relation between a too node
+    
+    The subject is expected to be a newer version of the object"""
+    sub = repo[subject]
+    obj = repo[object]
+    repo.addobsolete(sub.node(), obj.node())
+    return 0
+
+cmdtable = {'debugobsolete': (cmddebugobsolete, [], '<subject> <object>')}
+
+def reposetup(ui, repo):
+
+    if not repo.local():
+        return
+
+    opull = repo.pull
+    opush = repo.push
+
+    class obsoletingrepo(repo.__class__):
+
+
+        ### Hidden revision support
+        @util.propertycache
+        def hiddenrevs(self):
+            # It's a property because It simpler that to handle the __init__
+            revs = set()
+            return revs
+
+        ### obsolete storage
+        @util.propertycache
+        def _obsobjrels(self):
+            """{<old-node> -> set(<new-node>)}
+
+            also compute hidden revision"""
+            #reverse sub -> objs mapping
+            objrels = {}
+            for sub, objs in self._obssubrels.iteritems():
+                for obj in objs:
+                    objrels.setdefault(obj, set()).add(sub)
+            return objrels
+
+        @util.propertycache
+        def _obssubrels(self):
+            """{<new-node> -> set(<old-node>)}"""
+            return self._readobsrels()
+
+
+        ### Disk IO
+        def _readobsrels(self):
+            """Write obsolete relation on disk"""
+            # XXX handle lock
+            rels = {}
+            try:
+                f = self.opener('obsolete-relations')
+                try:
+                    for line in f:
+                        subhex, objhex = line.split()
+                        rels.setdefault(bin(subhex), set()).add(bin(objhex))
+                finally:
+                    f.close()
+            except IOError:
+                pass
+            return rels
+
+        def _writeobsrels(self):
+            """Write obsolete relation on disk"""
+            # XXX handle lock
+            f = self.opener('obsolete-relations', 'w', atomictemp=True)
+            try:
+                for sub, objs in self._obssubrels.iteritems():
+                    for obj in objs:
+                        f.write('%s %s\n' % (hex(sub), hex(obj)))
+                f.rename()
+            finally:
+                f.close()
+
+        ### local clone support
+
+        def cancopy(self):
+            return not bool(self._obsobjrels) # you can't copy if there is obsolete
+
+        ### pull // push support
+
+        def pull(self, remote, *args, **kwargs):
+            obskey = remote.listkeys('obsolete')
+            obsrels = obskey.get('relations', {})
+            result = opull(remote, *args, **kwargs)
+            for sub, objs in obsrels.iteritems():
+                for obj in objs:
+                    self.addobsolete(sub, obj)
+            return result
+
+        def push(self, remote, *args, **opts):
+            obskey = remote.listkeys('obsolete')
+            obssupport = 'relations' in obskey
+            result = opush(remote, *args, **opts)
+            if obssupport:
+                remote.pushkey('obsolete', 'relations', {}, self._obssubrels)
+            return result
+
+
+        ### Public method
+        def obsoletedby(self, node):
+            """return the set of node that make <node> obsolete (obj)"""
+            return self._obsobjrels.get(node, set())
+
+        def obsolete(self, node):
+            """return the set of node that <node> make obsolete (sub)"""
+            return self._obssubrels.get(node, set())
+
+        def addobsolete(self, sub, obj):
+            """Add a relation marking that node <sub> is a new version of <obj>"""
+            self._obssubrels.setdefault(sub, set()).add(obj)
+            self._obsobjrels.setdefault(obj, set()).add(sub)
+            try:
+                self.changelog.hiddenrevs.add(repo[obj].rev())
+            except error.RepoLookupError:
+                pass #unknow revision (but keep propagating the data
+            self._writeobsrels()
+
+    repo.__class__ = obsoletingrepo
+
+
+### Other Extension compat
+############################
+
+def concludenode(orig, repo, rev, *args, **kwargs):
+    newrev = orig(repo, rev, *args, **kwargs)
+    oldnode = repo[rev].node()
+    newnode = repo[newrev].node()
+    repo.addobsolete(newnode, oldnode)
+    return newrev
+