Patchwork [8,of,8,sparse,V2] dirstate: integrate sparse matcher with _ignore (API)

login
register
mail settings
Submitter Gregory Szorc
Date July 11, 2017, 4:57 a.m.
Message ID <7c09adee4cacd9086467.1499749027@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/22220/
State Accepted
Headers show

Comments

Gregory Szorc - July 11, 2017, 4:57 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1499555309 25200
#      Sat Jul 08 16:08:29 2017 -0700
# Node ID 7c09adee4cacd9086467998030ea0e0bc3fbabed
# Parent  f0ef4aff7559a680e216407e5822e3e281ef1a5f
dirstate: integrate sparse matcher with _ignore (API)

Previously, the sparse extension monkeypatched dirstate._ignore
(using exotic means because dirstate._ignore was a file cache
property).

In this commit, we move the logic from dirstate_ignore to a new method,
_hgignorematch(). We replace dirstate._ignore with a regular property.
The implementation of that property obtains a sparse matcher. If one
is defined, it returns a unionmatcher.

The implementation is funcionally equivalent to the old code in
the sparse extension. However, the property caching of dirstate._ignore
has been dropped. This means that every attribute lookup will:

* resolve a sparse matcher by calling sparse.matcher()
* call matcher.always()
* possibly construct a new unionmatcher if the sparse matcher is
  defined

The overhead is unfortunate. However, it is simple. sparse.matcher()
should be relatively fast. And I have plans for caching the sparse
matcher more intelligently to avoid all these lookups. Furthermore,
I don't see any obvious code paths where dirstate._ignore is resolved
in tight loops, so the perf impact may be mostly irrelevant.

After this commit, the sparse extension now only contains modifications
to commands: all code directly supporting sparse checkouts is now in
core!

Patch

diff --git a/hgext/sparse.py b/hgext/sparse.py
--- a/hgext/sparse.py
+++ b/hgext/sparse.py
@@ -78,11 +78,9 @@  from mercurial.i18n import _
 from mercurial import (
     cmdutil,
     commands,
-    dirstate,
     error,
     extensions,
     hg,
-    match as matchmod,
     registrar,
     sparse,
     util,
@@ -103,23 +101,6 @@  def extsetup(ui):
     _setupclone(ui)
     _setuplog(ui)
     _setupadd(ui)
-    _setupdirstate(ui)
-
-def replacefilecache(cls, propname, replacement):
-    """Replace a filecache property with a new class. This allows changing the
-    cache invalidation condition."""
-    origcls = cls
-    assert callable(replacement)
-    while cls is not object:
-        if propname in cls.__dict__:
-            orig = cls.__dict__[propname]
-            setattr(cls, propname, replacement(orig))
-            break
-        cls = cls.__bases__[0]
-
-    if cls is object:
-        raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
-                             propname))
 
 def _setuplog(ui):
     entry = commands.table['^log|history']
@@ -187,42 +168,6 @@  def _setupadd(ui):
 
     extensions.wrapcommand(commands.table, 'add', _add)
 
-def _setupdirstate(ui):
-    """Modify the dirstate to prevent stat'ing excluded files,
-    and to prevent modifications to files outside the checkout.
-    """
-
-    # The atrocity below is needed to wrap dirstate._ignore. It is a cached
-    # property, which means normal function wrapping doesn't work.
-    class ignorewrapper(object):
-        def __init__(self, orig):
-            self.orig = orig
-            self.origignore = None
-            self.func = None
-            self.sparsematch = None
-
-        def __get__(self, obj, type=None):
-            origignore = self.orig.__get__(obj)
-
-            sparsematch = obj._sparsematcher
-            if sparsematch.always():
-                return origignore
-
-            if self.sparsematch != sparsematch or self.origignore != origignore:
-                self.func = matchmod.unionmatcher([
-                    origignore, matchmod.negatematcher(sparsematch)])
-                self.sparsematch = sparsematch
-                self.origignore = origignore
-            return self.func
-
-        def __set__(self, obj, value):
-            return self.orig.__set__(obj, value)
-
-        def __delete__(self, obj):
-            return self.orig.__delete__(obj)
-
-    replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
-
 @command('^debugsparse', [
     ('I', 'include', False, _('include files in the sparse checkout')),
     ('X', 'exclude', False, _('exclude files in the sparse checkout')),
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -248,7 +248,7 @@  class dirstate(object):
         return self._dirs
 
     @rootcache('.hgignore')
-    def _ignore(self):
+    def _hgignorematch(self):
         files = self._ignorefiles()
         if not files:
             return matchmod.never(self._root, '')
@@ -256,6 +256,15 @@  class dirstate(object):
         pats = ['include:%s' % f for f in files]
         return matchmod.match(self._root, '', [], pats, warn=self._ui.warn)
 
+    @property
+    def _ignore(self):
+        sparsematch = self._sparsematcher
+        if sparsematch.always():
+            return self._hgignorematch
+
+        return matchmod.unionmatcher([self._hgignorematch,
+                                      matchmod.negatematcher(sparsematch)])
+
     @propertycache
     def _slash(self):
         return self._ui.configbool('ui', 'slash') and pycompat.ossep != '/'
@@ -516,7 +525,7 @@  class dirstate(object):
 
         for a in ("_map", "_copymap", "_identity",
                   "_filefoldmap", "_dirfoldmap", "_branch",
-                  "_pl", "_dirs", "_ignore", "_nonnormalset",
+                  "_pl", "_dirs", "_hgignorematcher", "_nonnormalset",
                   "_otherparentset"):
             if a in self.__dict__:
                 delattr(self, a)