[PATCH 12 of 17 RFC] clfilter: introduce a `revsfilter` class to control changelog filtering on repo

pierre-yves.david at logilab.fr pierre-yves.david at logilab.fr
Mon Sep 3 07:58:36 CDT 2012


# HG changeset patch
# User Pierre-Yves David <pierre-yves.david at logilab.fr>
# Date 1346675952 -7200
# Node ID c231611a44cef373a4f8da1add8346fa7f368b96
# Parent  96c8e124f220d706f05b5a3643945e2ba5266644
clfilter: introduce a `revsfilter` class to control changelog filtering on repo

Every local repository object get one instance in its `repo.revsfilter` attribute.

The filter is enabled by calling `repo.revsfilter.set(<name>)` were name is the
name of the filter (string). There is no valid filter for now but the "hidden"
and "unserved" are introduced by followup.  None means unfiltered. This function
returns a callback to reinstall the old filter that the call replaces (kind of
context manager).

Every method needs the repo, so we keep a reference to the repo in the object
for the sake of simplicity. To avoid a circular reference we use a weakref. This
should not introduce any bug as this object is always called through its repo.

Multiple logics need to run unfiltered to work:
- phase computation
- verify
- strip XXX (not enforced yet)
- bundle creation and consumption

Code that can lead to invalid cache for filter call the `invalidatefilter`
methods. (Note: I likely forgot several places were the cache should be
invalidated but this is not caught by the test yet.)

diff --git a/mercurial/context.py b/mercurial/context.py
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -92,13 +92,20 @@ class changectx(object):
             self._rev = repo.changelog.rev(self._node)
             return
 
         # lookup failed
         # check if it might have come from damaged dirstate
-        if changeid in repo.dirstate.parents():
-            raise error.Abort(_("working directory has unknown parent '%s'!")
-                              % short(changeid))
+        #
+        # XXX we could avoid the unfiltered if we had a recognisable exception
+        # for filtered changeset access
+        refilter = repo.revsfilter.set(None)
+        try:
+            if changeid in repo.dirstate.parents():
+                msg = _("working directory has unknown parent '%s'!")
+                raise error.Abort(msg % short(changeid))
+        finally:
+            refilter()
         try:
             if len(changeid) == 20:
                 changeid = hex(changeid)
         except TypeError:
             pass
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -6,11 +6,11 @@
 # GNU General Public License version 2 or any later version.
 from node import bin, hex, nullid, nullrev, short
 from i18n import _
 import peer, changegroup, subrepo, discovery, pushkey, obsolete
 import changelog, dirstate, filelog, manifest, context, bookmarks, phases
-import lock, transaction, store, encoding, base85
+import lock, transaction, store, encoding, base85, repofilter
 import scmutil, util, extensions, hook, error, revset
 import match as matchmod
 import merge as mergemod
 import tags as tagsmod
 from lock import release
@@ -203,10 +203,12 @@ class localrepository(object):
         # (used by the filecache decorator)
         #
         # Maps a property name to its util.filecacheentry
         self._filecache = {}
 
+        self.revsfilter = repofilter.revsfilter(self)
+
     def close(self):
         pass
 
     def _restrictcapabilities(self, caps):
         return caps
@@ -1050,10 +1052,11 @@ class localrepository(object):
 
         delcache('_tagscache')
 
         self._branchcache = None # in UTF-8
         self._branchcachetip = None
+        self.revsfilter.invalidatefilter()
 
     def invalidatedirstate(self):
         '''Invalidates the dirstate, causing the next call to dirstate
         to check if it was modified since the last time it was read,
         rereading it if it has.
@@ -1811,10 +1814,11 @@ class localrepository(object):
                         tr = self.transaction(trname)
                     for key in sorted(remoteobs, reverse=True):
                         if key.startswith('dump'):
                             data = base85.b85decode(remoteobs[key])
                             self.obsstore.mergemarkers(tr, data)
+                    self.revsfilter.invalidatefilter()
             if tr is not None:
                 tr.close()
         finally:
             if tr is not None:
                 tr.release()
@@ -1862,11 +1866,15 @@ class localrepository(object):
                 commoninc = fci(self, remote, force=force)
                 common, inc, remoteheads = commoninc
                 fco = discovery.findcommonoutgoing
                 outgoing = fco(self, remote, onlyheads=revs,
                                commoninc=commoninc, force=force)
-
+                refilter = self.revsfilter.set(None)
+                try:
+                    outgoing.missing
+                finally:
+                    refilter()
 
                 if not outgoing.missing:
                     # nothing to push
                     scmutil.nochangesfound(self.ui, self, outgoing.excluded)
                     ret = None
@@ -1892,11 +1900,12 @@ class localrepository(object):
                         discovery.checkheads(self, remote, outgoing,
                                              remoteheads, newbranch,
                                              bool(inc))
 
                     # create a changegroup from local
-                    if revs is None and not outgoing.excluded:
+                    # XXX XXX XXX XXX XXX XXX
+                    if False and revs is None and not outgoing.excluded:
                         # push everything,
                         # use the fast path, no race possible on push
                         cg = self._changegroup(outgoing.missing, 'push')
                     else:
                         cg = self.getlocalbundle('push', outgoing)
@@ -2082,12 +2091,16 @@ class localrepository(object):
         fstate = ['', {}]
         count = [0, 0]
 
         # can we go through the fast path ?
         heads.sort()
-        if heads == sorted(self.heads()):
-            return self._changegroup(csets, source)
+        refilter = self.revsfilter.set(None)
+        try:
+            if heads == sorted(self.heads()):
+                return self._changegroup(csets, source)
+        finally:
+            refilter()
 
         # slow path
         self.hook('preoutgoing', throw=True, source=source)
         self.changegroupinfo(csets, source)
 
@@ -2307,10 +2320,11 @@ class localrepository(object):
         cl = self.changelog
         cl.delayupdate()
         oldheads = cl.heads()
 
         tr = self.transaction("\n".join([srctype, util.hidepassword(url)]))
+        refilter = None
         try:
             trp = weakref.proxy(tr)
             # pull off the changeset group
             self.ui.status(_("adding changesets\n"))
             clstart = len(cl)
@@ -2325,10 +2339,11 @@ class localrepository(object):
                     self.count += 1
             pr = prog()
             source.callback = pr
 
             source.changelogheader()
+            refilter = self.revsfilter.set(None)
             srccontent = cl.addgroup(source, csmap, trp)
             if not (srccontent or emptyok):
                 raise util.Abort(_("received changelog group is empty"))
             clend = len(cl)
             changesets = clend - clstart
@@ -2412,10 +2427,11 @@ class localrepository(object):
                 htext = _(" (%+d heads)") % dh
 
             self.ui.status(_("added %d changesets"
                              " with %d changes to %d files%s\n")
                              % (changesets, revisions, files, htext))
+            self.revsfilter.invalidatefilter()
 
             if changesets > 0:
                 p = lambda: cl.writepending() and self.root or ""
                 self.hook('pretxnchangegroup', throw=True,
                           node=hex(cl.node(clstart)), source=srctype,
@@ -2459,10 +2475,12 @@ class localrepository(object):
                                   url=url)
                 self._afterlock(runhooks)
 
         finally:
             tr.release()
+            if refilter is not None:
+                refilter()
         # never return 0 here:
         if dh < 0:
             return dh - 1
         else:
             return dh + 1
diff --git a/mercurial/phases.py b/mercurial/phases.py
--- a/mercurial/phases.py
+++ b/mercurial/phases.py
@@ -181,19 +181,23 @@ class phasecache(object):
         for a in 'phaseroots dirty opener _phaserevs'.split():
             setattr(self, a, getattr(phcache, a))
 
     def getphaserevs(self, repo, rebuild=False):
         if rebuild or self._phaserevs is None:
-            revs = [public] * len(repo.changelog)
-            for phase in trackedphases:
-                roots = map(repo.changelog.rev, self.phaseroots[phase])
-                if roots:
-                    for rev in roots:
-                        revs[rev] = phase
-                    for rev in repo.changelog.descendants(roots):
-                        revs[rev] = phase
-            self._phaserevs = revs
+            refilter = repo.revsfilter.set(None)
+            try:
+                revs = [public] * len(repo.changelog)
+                for phase in trackedphases:
+                    roots = map(repo.changelog.rev, self.phaseroots[phase])
+                    if roots:
+                        for rev in roots:
+                            revs[rev] = phase
+                        for rev in repo.changelog.descendants(roots):
+                            revs[rev] = phase
+                self._phaserevs = revs
+            finally:
+                refilter()
         return self._phaserevs
 
     def phase(self, repo, rev):
         # We need a repo argument here to be able to build _phaserevs
         # if necessary. The repository instance is not stored in
@@ -224,44 +228,53 @@ class phasecache(object):
         self.dirty = True
 
     def advanceboundary(self, repo, targetphase, nodes):
         # Be careful to preserve shallow-copied values: do not update
         # phaseroots values, replace them.
-
-        delroots = [] # set of root deleted by this path
-        for phase in xrange(targetphase + 1, len(allphases)):
-            # filter nodes that are not in a compatible phase already
-            nodes = [n for n in nodes
-                     if self.phase(repo, repo[n].rev()) >= phase]
-            if not nodes:
-                break # no roots to move anymore
-            olds = self.phaseroots[phase]
-            roots = set(ctx.node() for ctx in repo.set(
-                    'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
-            if olds != roots:
-                self._updateroots(phase, roots)
-                # some roots may need to be declared for lower phases
-                delroots.extend(olds - roots)
-            # declare deleted root in the target phase
-            if targetphase != 0:
-                self.retractboundary(repo, targetphase, delroots)
+        refilter = repo.revsfilter.set(None)
+        try:
+            delroots = [] # set of root deleted by this path
+            for phase in xrange(targetphase + 1, len(allphases)):
+                # filter nodes that are not in a compatible phase already
+                nodes = [n for n in nodes
+                         if self.phase(repo, repo[n].rev()) >= phase]
+                if not nodes:
+                    break # no roots to move anymore
+                olds = self.phaseroots[phase]
+                roots = set(ctx.node() for ctx in repo.set(
+                        'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
+                if olds != roots:
+                    self._updateroots(phase, roots)
+                    # some roots may need to be declared for lower phases
+                    delroots.extend(olds - roots)
+                # declare deleted root in the target phase
+                if targetphase != 0:
+                    self.retractboundary(repo, targetphase, delroots)
+            repo.revsfilter.invalidatefilter()
+        finally:
+            refilter()
 
     def retractboundary(self, repo, targetphase, nodes):
         # Be careful to preserve shallow-copied values: do not update
         # phaseroots values, replace them.
 
-        currentroots = self.phaseroots[targetphase]
-        newroots = [n for n in nodes
-                    if self.phase(repo, repo[n].rev()) < targetphase]
-        if newroots:
-            if nullid in newroots:
-                raise util.Abort(_('cannot change null revision phase'))
-            currentroots = currentroots.copy()
-            currentroots.update(newroots)
-            ctxs = repo.set('roots(%ln::)', currentroots)
-            currentroots.intersection_update(ctx.node() for ctx in ctxs)
-            self._updateroots(targetphase, currentroots)
+        refilter = repo.revsfilter.set(None)
+        try:
+            currentroots = self.phaseroots[targetphase]
+            newroots = [n for n in nodes
+                        if self.phase(repo, repo[n].rev()) < targetphase]
+            if newroots:
+                if nullid in newroots:
+                    raise util.Abort(_('cannot change null revision phase'))
+                currentroots = currentroots.copy()
+                currentroots.update(newroots)
+                ctxs = repo.set('roots(%ln::)', currentroots)
+                currentroots.intersection_update(ctx.node() for ctx in ctxs)
+                self._updateroots(targetphase, currentroots)
+            repo.revsfilter.invalidatefilter()
+        finally:
+            refilter()
 
 def advanceboundary(repo, targetphase, nodes):
     """Add nodes to a phase changing other nodes phases if necessary.
 
     This function move boundary *forward* this means that all nodes
diff --git a/mercurial/repofilter.py b/mercurial/repofilter.py
new file mode 100644
--- /dev/null
+++ b/mercurial/repofilter.py
@@ -0,0 +1,60 @@
+# filter -- repository filtering
+#
+# Copyright 2012 Pierre-Yves David <pierre-yves.david at ens-lyon.org>
+#                Logilab SA        <contact at 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.
+
+import weakref
+
+computefiltered = {}
+
+
+class revsfilter(object):
+
+    def __init__(self, repo):
+        self._name = None
+        self._cache = {}
+        self._repo = weakref.proxy(repo)
+
+    def set(self, filtername):
+        """set a filter for the repository
+
+        If name is None, no filtering is performed
+
+        returns a callback to reinstall the previous filter"""
+        oldfilter = self._name
+        self._name = filtername
+        self._installfilter()
+        def exit():
+            self.set(oldfilter)
+        return exit
+
+    def invalidatefilter(self):
+        """invalidate current filter
+
+        called when something might have changed the filter value:
+        - new changesets,
+        - phase change,
+        - new obsolescence marker,
+        - working directory parent change,
+        - bookmark changes"""
+        self._cache.clear()
+        self._installfilter()
+
+    def _installfilter(self):
+        """align changelog.filtered with current repo.revsfiltering"""
+        repo = self._repo
+        name = self._name
+        if 'changelog' in repo._filecache:
+            self._repo.changelog.filteredrevs = ()
+        if name is None:
+            return
+        if not name in self._cache:
+            # compute the new filter unfiltered
+            self._name = None
+            func = computefiltered[name]
+            self._cache[name] = func(repo)
+            self._name = name
+        self._repo.changelog.filteredrevs = self._cache[name]
diff --git a/mercurial/statichttprepo.py b/mercurial/statichttprepo.py
--- a/mercurial/statichttprepo.py
+++ b/mercurial/statichttprepo.py
@@ -8,11 +8,11 @@
 # GNU General Public License version 2 or any later version.
 
 from i18n import _
 import changelog, byterange, url, error
 import localrepo, manifest, util, scmutil, store
-import urllib, urllib2, errno
+import urllib, urllib2, errno, repofilter
 
 class httprangereader(object):
     def __init__(self, url, opener):
         # we assume opener has HTTPRangeHandler
         self.url = url
@@ -130,10 +130,11 @@ class statichttprepository(localrepo.loc
         self.nodetagscache = None
         self._branchcache = None
         self._branchcachetip = None
         self.encodepats = None
         self.decodepats = None
+        self.revsfilter = repofilter.revsfilter(self)
 
     def _restrictcapabilities(self, caps):
         return caps.difference(["pushkey"])
 
     def url(self):
diff --git a/mercurial/verify.py b/mercurial/verify.py
--- a/mercurial/verify.py
+++ b/mercurial/verify.py
@@ -11,11 +11,15 @@ import os
 import revlog, util, error
 
 def verify(repo):
     lock = repo.lock()
     try:
-        return _verify(repo)
+        refilter = repo.revsfilter.set(None)
+        try:
+            return _verify(repo)
+        finally:
+            refilter()
     finally:
         lock.release()
 
 def _verify(repo):
     mflinkrevs = {}


More information about the Mercurial-devel mailing list