diff --git a/hgext/largefiles/lfutil.py b/hgext/largefiles/lfutil.py
--- a/hgext/largefiles/lfutil.py
+++ b/hgext/largefiles/lfutil.py
@@ -26,6 +26,7 @@ from mercurial import (
     node,
     pycompat,
     scmutil,
+    sparse,
     util,
     vfs as vfsmod,
 )
@@ -147,7 +148,8 @@ def openlfdirstate(ui, repo, create=True
     lfstoredir = longname
     opener = vfsmod.vfs(vfs.join(lfstoredir))
     lfdirstate = largefilesdirstate(opener, ui, repo.root,
-                                     repo.dirstate._validate)
+                                    repo.dirstate._validate,
+                                    lambda: sparse.matcher(repo))
 
     # If the largefiles dirstate does not exist, populate and create
     # it. This ensures that we create it on the first meaningful
diff --git a/hgext/sparse.py b/hgext/sparse.py
--- a/hgext/sparse.py
+++ b/hgext/sparse.py
@@ -82,7 +82,6 @@ from mercurial import (
     error,
     extensions,
     hg,
-    localrepo,
     match as matchmod,
     registrar,
     sparse,
@@ -106,13 +105,6 @@ def extsetup(ui):
     _setupadd(ui)
     _setupdirstate(ui)
 
-def reposetup(ui, repo):
-    if not util.safehasattr(repo, 'dirstate'):
-        return
-
-    if 'dirstate' in repo._filecache:
-        repo.dirstate.repo = repo
-
 def replacefilecache(cls, propname, replacement):
     """Replace a filecache property with a new class. This allows changing the
     cache invalidation condition."""
@@ -200,13 +192,6 @@ def _setupdirstate(ui):
     and to prevent modifications to files outside the checkout.
     """
 
-    def _dirstate(orig, repo):
-        dirstate = orig(repo)
-        dirstate.repo = repo
-        return dirstate
-    extensions.wrapfunction(
-        localrepo.localrepository.dirstate, 'func', _dirstate)
-
     # 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):
@@ -217,10 +202,9 @@ def _setupdirstate(ui):
             self.sparsematch = None
 
         def __get__(self, obj, type=None):
-            repo = obj.repo
             origignore = self.orig.__get__(obj)
 
-            sparsematch = sparse.matcher(repo)
+            sparsematch = obj._sparsematcher
             if sparsematch.always():
                 return origignore
 
@@ -241,7 +225,7 @@ def _setupdirstate(ui):
 
     # dirstate.rebuild should not add non-matching files
     def _rebuild(orig, self, parent, allfiles, changedfiles=None):
-        matcher = sparse.matcher(self.repo)
+        matcher = self._sparsematcher
         if not matcher.always():
             allfiles = allfiles.matches(matcher)
             if changedfiles:
@@ -262,8 +246,7 @@ def _setupdirstate(ui):
              '`hg add -s <file>` to include file directory while adding')
     for func in editfuncs:
         def _wrapper(orig, self, *args):
-            repo = self.repo
-            sparsematch = sparse.matcher(repo)
+            sparsematch = self._sparsematcher
             if not sparsematch.always():
                 for f in args:
                     if (f is not None and not sparsematch(f) and
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -70,7 +70,7 @@ def nonnormalentries(dmap):
 
 class dirstate(object):
 
-    def __init__(self, opener, ui, root, validate):
+    def __init__(self, opener, ui, root, validate, sparsematchfn):
         '''Create a new dirstate object.
 
         opener is an open()-like callable that can be used to open the
@@ -80,6 +80,7 @@ class dirstate(object):
         self._opener = opener
         self._validate = validate
         self._root = root
+        self._sparsematchfn = sparsematchfn
         # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
         # UNC path pointing to root share (issue4557)
         self._rootdir = pathutil.normasprefix(root)
@@ -197,6 +198,19 @@ class dirstate(object):
             f[normcase(name)] = name
         return f
 
+    @property
+    def _sparsematcher(self):
+        """The matcher for the sparse checkout.
+
+        The working directory may not include every file from a manifest. The
+        matcher obtained by this property will match a path if it is to be
+        included in the working directory.
+        """
+        # TODO there is potential to cache this property. For now, the matcher
+        # is resolved on every access. (But the called function does use a
+        # cache to keep the lookup fast.)
+        return self._sparsematchfn()
+
     @repocache('branch')
     def _branch(self):
         try:
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -53,6 +53,7 @@ from . import (
     revset,
     revsetlang,
     scmutil,
+    sparse,
     store,
     subrepo,
     tags as tagsmod,
@@ -570,8 +571,10 @@ class localrepository(object):
 
     @repofilecache('dirstate')
     def dirstate(self):
+        sparsematchfn = lambda: sparse.matcher(self)
+
         return dirstate.dirstate(self.vfs, self.ui, self.root,
-                                 self._dirstatevalidate)
+                                 self._dirstatevalidate, sparsematchfn)
 
     def _dirstatevalidate(self, node):
         try: