diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -19,6 +19,7 @@
 from hgdemandimport import tracing
 
 from . import (
+    dirstatemap,
     encoding,
     error,
     match as matchmod,
@@ -27,7 +28,6 @@
     pycompat,
     scmutil,
     sparse,
-    txnutil,
     util,
 )
 
@@ -49,13 +49,13 @@
 
 
 # a special value used internally for `size` if the file come from the other parent
-FROM_P2 = -2
+FROM_P2 = dirstatemap.FROM_P2
 
 # a special value used internally for `size` if the file is modified/merged/added
-NONNORMAL = -1
+NONNORMAL = dirstatemap.NONNORMAL
 
 # a special value used internally for `time` if the time is ambigeous
-AMBIGUOUS_TIME = -1
+AMBIGUOUS_TIME = dirstatemap.AMBIGUOUS_TIME
 
 
 class repocache(filecache):
@@ -119,7 +119,7 @@
         self._plchangecallbacks = {}
         self._origpl = None
         self._updatedfiles = set()
-        self._mapcls = dirstatemap
+        self._mapcls = dirstatemap.dirstatemap
         # Access and cache cwd early, so we don't access it for the first time
         # after a working-copy update caused it to not exist (accessing it then
         # raises an exception).
@@ -1450,567 +1450,3 @@
     def clearbackup(self, tr, backupname):
         '''Clear backup file'''
         self._opener.unlink(backupname)
-
-
-class dirstatemap(object):
-    """Map encapsulating the dirstate's contents.
-
-    The dirstate contains the following state:
-
-    - `identity` is the identity of the dirstate file, which can be used to
-      detect when changes have occurred to the dirstate file.
-
-    - `parents` is a pair containing the parents of the working copy. The
-      parents are updated by calling `setparents`.
-
-    - the state map maps filenames to tuples of (state, mode, size, mtime),
-      where state is a single character representing 'normal', 'added',
-      'removed', or 'merged'. It is read by treating the dirstate as a
-      dict.  File state is updated by calling the `addfile`, `removefile` and
-      `dropfile` methods.
-
-    - `copymap` maps destination filenames to their source filename.
-
-    The dirstate also provides the following views onto the state:
-
-    - `nonnormalset` is a set of the filenames that have state other
-      than 'normal', or are normal but have an mtime of -1 ('normallookup').
-
-    - `otherparentset` is a set of the filenames that are marked as coming
-      from the second parent when the dirstate is currently being merged.
-
-    - `filefoldmap` is a dict mapping normalized filenames to the denormalized
-      form that they appear as in the dirstate.
-
-    - `dirfoldmap` is a dict mapping normalized directory names to the
-      denormalized form that they appear as in the dirstate.
-    """
-
-    def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
-        self._ui = ui
-        self._opener = opener
-        self._root = root
-        self._filename = b'dirstate'
-        self._nodelen = 20
-        self._nodeconstants = nodeconstants
-        assert (
-            not use_dirstate_v2
-        ), "should have detected unsupported requirement"
-
-        self._parents = None
-        self._dirtyparents = False
-
-        # for consistent view between _pl() and _read() invocations
-        self._pendingmode = None
-
-    @propertycache
-    def _map(self):
-        self._map = {}
-        self.read()
-        return self._map
-
-    @propertycache
-    def copymap(self):
-        self.copymap = {}
-        self._map
-        return self.copymap
-
-    def directories(self):
-        # Rust / dirstate-v2 only
-        return []
-
-    def clear(self):
-        self._map.clear()
-        self.copymap.clear()
-        self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
-        util.clearcachedproperty(self, b"_dirs")
-        util.clearcachedproperty(self, b"_alldirs")
-        util.clearcachedproperty(self, b"filefoldmap")
-        util.clearcachedproperty(self, b"dirfoldmap")
-        util.clearcachedproperty(self, b"nonnormalset")
-        util.clearcachedproperty(self, b"otherparentset")
-
-    def items(self):
-        return pycompat.iteritems(self._map)
-
-    # forward for python2,3 compat
-    iteritems = items
-
-    def __len__(self):
-        return len(self._map)
-
-    def __iter__(self):
-        return iter(self._map)
-
-    def get(self, key, default=None):
-        return self._map.get(key, default)
-
-    def __contains__(self, key):
-        return key in self._map
-
-    def __getitem__(self, key):
-        return self._map[key]
-
-    def keys(self):
-        return self._map.keys()
-
-    def preload(self):
-        """Loads the underlying data, if it's not already loaded"""
-        self._map
-
-    def addfile(self, f, oldstate, state, mode, size, mtime):
-        """Add a tracked file to the dirstate."""
-        if oldstate in b"?r" and "_dirs" in self.__dict__:
-            self._dirs.addpath(f)
-        if oldstate == b"?" and "_alldirs" in self.__dict__:
-            self._alldirs.addpath(f)
-        self._map[f] = dirstatetuple(state, mode, size, mtime)
-        if state != b'n' or mtime == AMBIGUOUS_TIME:
-            self.nonnormalset.add(f)
-        if size == FROM_P2:
-            self.otherparentset.add(f)
-
-    def removefile(self, f, oldstate, size):
-        """
-        Mark a file as removed in the dirstate.
-
-        The `size` parameter is used to store sentinel values that indicate
-        the file's previous state.  In the future, we should refactor this
-        to be more explicit about what that state is.
-        """
-        if oldstate not in b"?r" and "_dirs" in self.__dict__:
-            self._dirs.delpath(f)
-        if oldstate == b"?" and "_alldirs" in self.__dict__:
-            self._alldirs.addpath(f)
-        if "filefoldmap" in self.__dict__:
-            normed = util.normcase(f)
-            self.filefoldmap.pop(normed, None)
-        self._map[f] = dirstatetuple(b'r', 0, size, 0)
-        self.nonnormalset.add(f)
-
-    def dropfile(self, f, oldstate):
-        """
-        Remove a file from the dirstate.  Returns True if the file was
-        previously recorded.
-        """
-        exists = self._map.pop(f, None) is not None
-        if exists:
-            if oldstate != b"r" and "_dirs" in self.__dict__:
-                self._dirs.delpath(f)
-            if "_alldirs" in self.__dict__:
-                self._alldirs.delpath(f)
-        if "filefoldmap" in self.__dict__:
-            normed = util.normcase(f)
-            self.filefoldmap.pop(normed, None)
-        self.nonnormalset.discard(f)
-        return exists
-
-    def clearambiguoustimes(self, files, now):
-        for f in files:
-            e = self.get(f)
-            if e is not None and e[0] == b'n' and e[3] == now:
-                self._map[f] = dirstatetuple(e[0], e[1], e[2], AMBIGUOUS_TIME)
-                self.nonnormalset.add(f)
-
-    def nonnormalentries(self):
-        '''Compute the nonnormal dirstate entries from the dmap'''
-        try:
-            return parsers.nonnormalotherparententries(self._map)
-        except AttributeError:
-            nonnorm = set()
-            otherparent = set()
-            for fname, e in pycompat.iteritems(self._map):
-                if e[0] != b'n' or e[3] == AMBIGUOUS_TIME:
-                    nonnorm.add(fname)
-                if e[0] == b'n' and e[2] == FROM_P2:
-                    otherparent.add(fname)
-            return nonnorm, otherparent
-
-    @propertycache
-    def filefoldmap(self):
-        """Returns a dictionary mapping normalized case paths to their
-        non-normalized versions.
-        """
-        try:
-            makefilefoldmap = parsers.make_file_foldmap
-        except AttributeError:
-            pass
-        else:
-            return makefilefoldmap(
-                self._map, util.normcasespec, util.normcasefallback
-            )
-
-        f = {}
-        normcase = util.normcase
-        for name, s in pycompat.iteritems(self._map):
-            if s[0] != b'r':
-                f[normcase(name)] = name
-        f[b'.'] = b'.'  # prevents useless util.fspath() invocation
-        return f
-
-    def hastrackeddir(self, d):
-        """
-        Returns True if the dirstate contains a tracked (not removed) file
-        in this directory.
-        """
-        return d in self._dirs
-
-    def hasdir(self, d):
-        """
-        Returns True if the dirstate contains a file (tracked or removed)
-        in this directory.
-        """
-        return d in self._alldirs
-
-    @propertycache
-    def _dirs(self):
-        return pathutil.dirs(self._map, b'r')
-
-    @propertycache
-    def _alldirs(self):
-        return pathutil.dirs(self._map)
-
-    def _opendirstatefile(self):
-        fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
-        if self._pendingmode is not None and self._pendingmode != mode:
-            fp.close()
-            raise error.Abort(
-                _(b'working directory state may be changed parallelly')
-            )
-        self._pendingmode = mode
-        return fp
-
-    def parents(self):
-        if not self._parents:
-            try:
-                fp = self._opendirstatefile()
-                st = fp.read(2 * self._nodelen)
-                fp.close()
-            except IOError as err:
-                if err.errno != errno.ENOENT:
-                    raise
-                # File doesn't exist, so the current state is empty
-                st = b''
-
-            l = len(st)
-            if l == self._nodelen * 2:
-                self._parents = (
-                    st[: self._nodelen],
-                    st[self._nodelen : 2 * self._nodelen],
-                )
-            elif l == 0:
-                self._parents = (
-                    self._nodeconstants.nullid,
-                    self._nodeconstants.nullid,
-                )
-            else:
-                raise error.Abort(
-                    _(b'working directory state appears damaged!')
-                )
-
-        return self._parents
-
-    def setparents(self, p1, p2):
-        self._parents = (p1, p2)
-        self._dirtyparents = True
-
-    def read(self):
-        # ignore HG_PENDING because identity is used only for writing
-        self.identity = util.filestat.frompath(
-            self._opener.join(self._filename)
-        )
-
-        try:
-            fp = self._opendirstatefile()
-            try:
-                st = fp.read()
-            finally:
-                fp.close()
-        except IOError as err:
-            if err.errno != errno.ENOENT:
-                raise
-            return
-        if not st:
-            return
-
-        if util.safehasattr(parsers, b'dict_new_presized'):
-            # Make an estimate of the number of files in the dirstate based on
-            # its size. This trades wasting some memory for avoiding costly
-            # resizes. Each entry have a prefix of 17 bytes followed by one or
-            # two path names. Studies on various large-scale real-world repositories
-            # found 54 bytes a reasonable upper limit for the average path names.
-            # Copy entries are ignored for the sake of this estimate.
-            self._map = parsers.dict_new_presized(len(st) // 71)
-
-        # Python's garbage collector triggers a GC each time a certain number
-        # of container objects (the number being defined by
-        # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
-        # for each file in the dirstate. The C version then immediately marks
-        # them as not to be tracked by the collector. However, this has no
-        # effect on when GCs are triggered, only on what objects the GC looks
-        # into. This means that O(number of files) GCs are unavoidable.
-        # Depending on when in the process's lifetime the dirstate is parsed,
-        # this can get very expensive. As a workaround, disable GC while
-        # parsing the dirstate.
-        #
-        # (we cannot decorate the function directly since it is in a C module)
-        parse_dirstate = util.nogc(parsers.parse_dirstate)
-        p = parse_dirstate(self._map, self.copymap, st)
-        if not self._dirtyparents:
-            self.setparents(*p)
-
-        # Avoid excess attribute lookups by fast pathing certain checks
-        self.__contains__ = self._map.__contains__
-        self.__getitem__ = self._map.__getitem__
-        self.get = self._map.get
-
-    def write(self, st, now):
-        st.write(
-            parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
-        )
-        st.close()
-        self._dirtyparents = False
-        self.nonnormalset, self.otherparentset = self.nonnormalentries()
-
-    @propertycache
-    def nonnormalset(self):
-        nonnorm, otherparents = self.nonnormalentries()
-        self.otherparentset = otherparents
-        return nonnorm
-
-    @propertycache
-    def otherparentset(self):
-        nonnorm, otherparents = self.nonnormalentries()
-        self.nonnormalset = nonnorm
-        return otherparents
-
-    def non_normal_or_other_parent_paths(self):
-        return self.nonnormalset.union(self.otherparentset)
-
-    @propertycache
-    def identity(self):
-        self._map
-        return self.identity
-
-    @propertycache
-    def dirfoldmap(self):
-        f = {}
-        normcase = util.normcase
-        for name in self._dirs:
-            f[normcase(name)] = name
-        return f
-
-
-if rustmod is not None:
-
-    class dirstatemap(object):
-        def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
-            self._use_dirstate_v2 = use_dirstate_v2
-            self._nodeconstants = nodeconstants
-            self._ui = ui
-            self._opener = opener
-            self._root = root
-            self._filename = b'dirstate'
-            self._nodelen = 20  # Also update Rust code when changing this!
-            self._parents = None
-            self._dirtyparents = False
-
-            # for consistent view between _pl() and _read() invocations
-            self._pendingmode = None
-
-            self._use_dirstate_tree = self._ui.configbool(
-                b"experimental",
-                b"dirstate-tree.in-memory",
-                False,
-            )
-
-        def addfile(self, *args, **kwargs):
-            return self._rustmap.addfile(*args, **kwargs)
-
-        def removefile(self, *args, **kwargs):
-            return self._rustmap.removefile(*args, **kwargs)
-
-        def dropfile(self, *args, **kwargs):
-            return self._rustmap.dropfile(*args, **kwargs)
-
-        def clearambiguoustimes(self, *args, **kwargs):
-            return self._rustmap.clearambiguoustimes(*args, **kwargs)
-
-        def nonnormalentries(self):
-            return self._rustmap.nonnormalentries()
-
-        def get(self, *args, **kwargs):
-            return self._rustmap.get(*args, **kwargs)
-
-        @property
-        def copymap(self):
-            return self._rustmap.copymap()
-
-        def directories(self):
-            return self._rustmap.directories()
-
-        def preload(self):
-            self._rustmap
-
-        def clear(self):
-            self._rustmap.clear()
-            self.setparents(
-                self._nodeconstants.nullid, self._nodeconstants.nullid
-            )
-            util.clearcachedproperty(self, b"_dirs")
-            util.clearcachedproperty(self, b"_alldirs")
-            util.clearcachedproperty(self, b"dirfoldmap")
-
-        def items(self):
-            return self._rustmap.items()
-
-        def keys(self):
-            return iter(self._rustmap)
-
-        def __contains__(self, key):
-            return key in self._rustmap
-
-        def __getitem__(self, item):
-            return self._rustmap[item]
-
-        def __len__(self):
-            return len(self._rustmap)
-
-        def __iter__(self):
-            return iter(self._rustmap)
-
-        # forward for python2,3 compat
-        iteritems = items
-
-        def _opendirstatefile(self):
-            fp, mode = txnutil.trypending(
-                self._root, self._opener, self._filename
-            )
-            if self._pendingmode is not None and self._pendingmode != mode:
-                fp.close()
-                raise error.Abort(
-                    _(b'working directory state may be changed parallelly')
-                )
-            self._pendingmode = mode
-            return fp
-
-        def setparents(self, p1, p2):
-            self._parents = (p1, p2)
-            self._dirtyparents = True
-
-        def parents(self):
-            if not self._parents:
-                if self._use_dirstate_v2:
-                    offset = len(rustmod.V2_FORMAT_MARKER)
-                else:
-                    offset = 0
-                read_len = offset + self._nodelen * 2
-                try:
-                    fp = self._opendirstatefile()
-                    st = fp.read(read_len)
-                    fp.close()
-                except IOError as err:
-                    if err.errno != errno.ENOENT:
-                        raise
-                    # File doesn't exist, so the current state is empty
-                    st = b''
-
-                l = len(st)
-                if l == read_len:
-                    st = st[offset:]
-                    self._parents = (
-                        st[: self._nodelen],
-                        st[self._nodelen : 2 * self._nodelen],
-                    )
-                elif l == 0:
-                    self._parents = (
-                        self._nodeconstants.nullid,
-                        self._nodeconstants.nullid,
-                    )
-                else:
-                    raise error.Abort(
-                        _(b'working directory state appears damaged!')
-                    )
-
-            return self._parents
-
-        @propertycache
-        def _rustmap(self):
-            """
-            Fills the Dirstatemap when called.
-            """
-            # ignore HG_PENDING because identity is used only for writing
-            self.identity = util.filestat.frompath(
-                self._opener.join(self._filename)
-            )
-
-            try:
-                fp = self._opendirstatefile()
-                try:
-                    st = fp.read()
-                finally:
-                    fp.close()
-            except IOError as err:
-                if err.errno != errno.ENOENT:
-                    raise
-                st = b''
-
-            self._rustmap, parents = rustmod.DirstateMap.new(
-                self._use_dirstate_tree, self._use_dirstate_v2, st
-            )
-
-            if parents and not self._dirtyparents:
-                self.setparents(*parents)
-
-            self.__contains__ = self._rustmap.__contains__
-            self.__getitem__ = self._rustmap.__getitem__
-            self.get = self._rustmap.get
-            return self._rustmap
-
-        def write(self, st, now):
-            parents = self.parents()
-            packed = self._rustmap.write(
-                self._use_dirstate_v2, parents[0], parents[1], now
-            )
-            st.write(packed)
-            st.close()
-            self._dirtyparents = False
-
-        @propertycache
-        def filefoldmap(self):
-            """Returns a dictionary mapping normalized case paths to their
-            non-normalized versions.
-            """
-            return self._rustmap.filefoldmapasdict()
-
-        def hastrackeddir(self, d):
-            return self._rustmap.hastrackeddir(d)
-
-        def hasdir(self, d):
-            return self._rustmap.hasdir(d)
-
-        @propertycache
-        def identity(self):
-            self._rustmap
-            return self.identity
-
-        @property
-        def nonnormalset(self):
-            nonnorm = self._rustmap.non_normal_entries()
-            return nonnorm
-
-        @propertycache
-        def otherparentset(self):
-            otherparents = self._rustmap.other_parent_entries()
-            return otherparents
-
-        def non_normal_or_other_parent_paths(self):
-            return self._rustmap.non_normal_or_other_parent_paths()
-
-        @propertycache
-        def dirfoldmap(self):
-            f = {}
-            normcase = util.normcase
-            for name, _pseudo_entry in self.directories():
-                f[normcase(name)] = name
-            return f
diff --git a/mercurial/dirstate.py b/mercurial/dirstatemap.py
copy from mercurial/dirstate.py
copy to mercurial/dirstatemap.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstatemap.py
@@ -1,49 +1,27 @@
-# dirstate.py - working directory tracking for mercurial
-#
-# Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
+# dirstatemap.py
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
 from __future__ import absolute_import
 
-import collections
-import contextlib
 import errno
-import os
-import stat
 
 from .i18n import _
-from .pycompat import delattr
-
-from hgdemandimport import tracing
 
 from . import (
-    encoding,
     error,
-    match as matchmod,
     pathutil,
     policy,
     pycompat,
-    scmutil,
-    sparse,
     txnutil,
     util,
 )
 
-from .interfaces import (
-    dirstate as intdirstate,
-    util as interfaceutil,
-)
-
 parsers = policy.importmod('parsers')
 rustmod = policy.importrust('dirstate')
 
-SUPPORTS_DIRSTATE_V2 = rustmod is not None
-
 propertycache = util.propertycache
-filecache = scmutil.filecache
-_rangemask = 0x7FFFFFFF
 
 dirstatetuple = parsers.dirstatetuple
 
@@ -58,1400 +36,6 @@
 AMBIGUOUS_TIME = -1
 
 
-class repocache(filecache):
-    """filecache for files in .hg/"""
-
-    def join(self, obj, fname):
-        return obj._opener.join(fname)
-
-
-class rootcache(filecache):
-    """filecache for files in the repository root"""
-
-    def join(self, obj, fname):
-        return obj._join(fname)
-
-
-def _getfsnow(vfs):
-    '''Get "now" timestamp on filesystem'''
-    tmpfd, tmpname = vfs.mkstemp()
-    try:
-        return os.fstat(tmpfd)[stat.ST_MTIME]
-    finally:
-        os.close(tmpfd)
-        vfs.unlink(tmpname)
-
-
-@interfaceutil.implementer(intdirstate.idirstate)
-class dirstate(object):
-    def __init__(
-        self,
-        opener,
-        ui,
-        root,
-        validate,
-        sparsematchfn,
-        nodeconstants,
-        use_dirstate_v2,
-    ):
-        """Create a new dirstate object.
-
-        opener is an open()-like callable that can be used to open the
-        dirstate file; root is the root of the directory tracked by
-        the dirstate.
-        """
-        self._use_dirstate_v2 = use_dirstate_v2
-        self._nodeconstants = nodeconstants
-        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)
-        self._dirty = False
-        self._lastnormaltime = 0
-        self._ui = ui
-        self._filecache = {}
-        self._parentwriters = 0
-        self._filename = b'dirstate'
-        self._pendingfilename = b'%s.pending' % self._filename
-        self._plchangecallbacks = {}
-        self._origpl = None
-        self._updatedfiles = set()
-        self._mapcls = dirstatemap
-        # Access and cache cwd early, so we don't access it for the first time
-        # after a working-copy update caused it to not exist (accessing it then
-        # raises an exception).
-        self._cwd
-
-    def prefetch_parents(self):
-        """make sure the parents are loaded
-
-        Used to avoid a race condition.
-        """
-        self._pl
-
-    @contextlib.contextmanager
-    def parentchange(self):
-        """Context manager for handling dirstate parents.
-
-        If an exception occurs in the scope of the context manager,
-        the incoherent dirstate won't be written when wlock is
-        released.
-        """
-        self._parentwriters += 1
-        yield
-        # Typically we want the "undo" step of a context manager in a
-        # finally block so it happens even when an exception
-        # occurs. In this case, however, we only want to decrement
-        # parentwriters if the code in the with statement exits
-        # normally, so we don't have a try/finally here on purpose.
-        self._parentwriters -= 1
-
-    def pendingparentchange(self):
-        """Returns true if the dirstate is in the middle of a set of changes
-        that modify the dirstate parent.
-        """
-        return self._parentwriters > 0
-
-    @propertycache
-    def _map(self):
-        """Return the dirstate contents (see documentation for dirstatemap)."""
-        self._map = self._mapcls(
-            self._ui,
-            self._opener,
-            self._root,
-            self._nodeconstants,
-            self._use_dirstate_v2,
-        )
-        return self._map
-
-    @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(b'branch')
-    def _branch(self):
-        try:
-            return self._opener.read(b"branch").strip() or b"default"
-        except IOError as inst:
-            if inst.errno != errno.ENOENT:
-                raise
-            return b"default"
-
-    @property
-    def _pl(self):
-        return self._map.parents()
-
-    def hasdir(self, d):
-        return self._map.hastrackeddir(d)
-
-    @rootcache(b'.hgignore')
-    def _ignore(self):
-        files = self._ignorefiles()
-        if not files:
-            return matchmod.never()
-
-        pats = [b'include:%s' % f for f in files]
-        return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
-
-    @propertycache
-    def _slash(self):
-        return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
-
-    @propertycache
-    def _checklink(self):
-        return util.checklink(self._root)
-
-    @propertycache
-    def _checkexec(self):
-        return bool(util.checkexec(self._root))
-
-    @propertycache
-    def _checkcase(self):
-        return not util.fscasesensitive(self._join(b'.hg'))
-
-    def _join(self, f):
-        # much faster than os.path.join()
-        # it's safe because f is always a relative path
-        return self._rootdir + f
-
-    def flagfunc(self, buildfallback):
-        if self._checklink and self._checkexec:
-
-            def f(x):
-                try:
-                    st = os.lstat(self._join(x))
-                    if util.statislink(st):
-                        return b'l'
-                    if util.statisexec(st):
-                        return b'x'
-                except OSError:
-                    pass
-                return b''
-
-            return f
-
-        fallback = buildfallback()
-        if self._checklink:
-
-            def f(x):
-                if os.path.islink(self._join(x)):
-                    return b'l'
-                if b'x' in fallback(x):
-                    return b'x'
-                return b''
-
-            return f
-        if self._checkexec:
-
-            def f(x):
-                if b'l' in fallback(x):
-                    return b'l'
-                if util.isexec(self._join(x)):
-                    return b'x'
-                return b''
-
-            return f
-        else:
-            return fallback
-
-    @propertycache
-    def _cwd(self):
-        # internal config: ui.forcecwd
-        forcecwd = self._ui.config(b'ui', b'forcecwd')
-        if forcecwd:
-            return forcecwd
-        return encoding.getcwd()
-
-    def getcwd(self):
-        """Return the path from which a canonical path is calculated.
-
-        This path should be used to resolve file patterns or to convert
-        canonical paths back to file paths for display. It shouldn't be
-        used to get real file paths. Use vfs functions instead.
-        """
-        cwd = self._cwd
-        if cwd == self._root:
-            return b''
-        # self._root ends with a path separator if self._root is '/' or 'C:\'
-        rootsep = self._root
-        if not util.endswithsep(rootsep):
-            rootsep += pycompat.ossep
-        if cwd.startswith(rootsep):
-            return cwd[len(rootsep) :]
-        else:
-            # we're outside the repo. return an absolute path.
-            return cwd
-
-    def pathto(self, f, cwd=None):
-        if cwd is None:
-            cwd = self.getcwd()
-        path = util.pathto(self._root, cwd, f)
-        if self._slash:
-            return util.pconvert(path)
-        return path
-
-    def __getitem__(self, key):
-        """Return the current state of key (a filename) in the dirstate.
-
-        States are:
-          n  normal
-          m  needs merging
-          r  marked for removal
-          a  marked for addition
-          ?  not tracked
-        """
-        return self._map.get(key, (b"?",))[0]
-
-    def __contains__(self, key):
-        return key in self._map
-
-    def __iter__(self):
-        return iter(sorted(self._map))
-
-    def items(self):
-        return pycompat.iteritems(self._map)
-
-    iteritems = items
-
-    def directories(self):
-        return self._map.directories()
-
-    def parents(self):
-        return [self._validate(p) for p in self._pl]
-
-    def p1(self):
-        return self._validate(self._pl[0])
-
-    def p2(self):
-        return self._validate(self._pl[1])
-
-    def branch(self):
-        return encoding.tolocal(self._branch)
-
-    def setparents(self, p1, p2=None):
-        """Set dirstate parents to p1 and p2.
-
-        When moving from two parents to one, 'm' merged entries a
-        adjusted to normal and previous copy records discarded and
-        returned by the call.
-
-        See localrepo.setparents()
-        """
-        if p2 is None:
-            p2 = self._nodeconstants.nullid
-        if self._parentwriters == 0:
-            raise ValueError(
-                b"cannot set dirstate parent outside of "
-                b"dirstate.parentchange context manager"
-            )
-
-        self._dirty = True
-        oldp2 = self._pl[1]
-        if self._origpl is None:
-            self._origpl = self._pl
-        self._map.setparents(p1, p2)
-        copies = {}
-        if (
-            oldp2 != self._nodeconstants.nullid
-            and p2 == self._nodeconstants.nullid
-        ):
-            candidatefiles = self._map.non_normal_or_other_parent_paths()
-
-            for f in candidatefiles:
-                s = self._map.get(f)
-                if s is None:
-                    continue
-
-                # Discard 'm' markers when moving away from a merge state
-                if s[0] == b'm':
-                    source = self._map.copymap.get(f)
-                    if source:
-                        copies[f] = source
-                    self.normallookup(f)
-                # Also fix up otherparent markers
-                elif s[0] == b'n' and s[2] == FROM_P2:
-                    source = self._map.copymap.get(f)
-                    if source:
-                        copies[f] = source
-                    self.add(f)
-        return copies
-
-    def setbranch(self, branch):
-        self.__class__._branch.set(self, encoding.fromlocal(branch))
-        f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
-        try:
-            f.write(self._branch + b'\n')
-            f.close()
-
-            # make sure filecache has the correct stat info for _branch after
-            # replacing the underlying file
-            ce = self._filecache[b'_branch']
-            if ce:
-                ce.refresh()
-        except:  # re-raises
-            f.discard()
-            raise
-
-    def invalidate(self):
-        """Causes the next access to reread the dirstate.
-
-        This is different from localrepo.invalidatedirstate() because it always
-        rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
-        check whether the dirstate has changed before rereading it."""
-
-        for a in ("_map", "_branch", "_ignore"):
-            if a in self.__dict__:
-                delattr(self, a)
-        self._lastnormaltime = 0
-        self._dirty = False
-        self._updatedfiles.clear()
-        self._parentwriters = 0
-        self._origpl = None
-
-    def copy(self, source, dest):
-        """Mark dest as a copy of source. Unmark dest if source is None."""
-        if source == dest:
-            return
-        self._dirty = True
-        if source is not None:
-            self._map.copymap[dest] = source
-            self._updatedfiles.add(source)
-            self._updatedfiles.add(dest)
-        elif self._map.copymap.pop(dest, None):
-            self._updatedfiles.add(dest)
-
-    def copied(self, file):
-        return self._map.copymap.get(file, None)
-
-    def copies(self):
-        return self._map.copymap
-
-    def _addpath(
-        self,
-        f,
-        state,
-        mode,
-        size=NONNORMAL,
-        mtime=AMBIGUOUS_TIME,
-        from_p2=False,
-        possibly_dirty=False,
-    ):
-        oldstate = self[f]
-        if state == b'a' or oldstate == b'r':
-            scmutil.checkfilename(f)
-            if self._map.hastrackeddir(f):
-                msg = _(b'directory %r already in dirstate')
-                msg %= pycompat.bytestr(f)
-                raise error.Abort(msg)
-            # shadows
-            for d in pathutil.finddirs(f):
-                if self._map.hastrackeddir(d):
-                    break
-                entry = self._map.get(d)
-                if entry is not None and entry[0] != b'r':
-                    msg = _(b'file %r in dirstate clashes with %r')
-                    msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
-                    raise error.Abort(msg)
-        if state == b'a':
-            assert not possibly_dirty
-            assert not from_p2
-            size = NONNORMAL
-            mtime = AMBIGUOUS_TIME
-        elif from_p2:
-            assert not possibly_dirty
-            size = FROM_P2
-            mtime = AMBIGUOUS_TIME
-        elif possibly_dirty:
-            mtime = AMBIGUOUS_TIME
-        else:
-            assert size != FROM_P2
-            assert size != NONNORMAL
-            size = size & _rangemask
-            mtime = mtime & _rangemask
-        self._dirty = True
-        self._updatedfiles.add(f)
-        self._map.addfile(f, oldstate, state, mode, size, mtime)
-
-    def normal(self, f, parentfiledata=None):
-        """Mark a file normal and clean.
-
-        parentfiledata: (mode, size, mtime) of the clean file
-
-        parentfiledata should be computed from memory (for mode,
-        size), as or close as possible from the point where we
-        determined the file was clean, to limit the risk of the
-        file having been changed by an external process between the
-        moment where the file was determined to be clean and now."""
-        if parentfiledata:
-            (mode, size, mtime) = parentfiledata
-        else:
-            s = os.lstat(self._join(f))
-            mode = s.st_mode
-            size = s.st_size
-            mtime = s[stat.ST_MTIME]
-        self._addpath(f, b'n', mode, size, mtime)
-        self._map.copymap.pop(f, None)
-        if f in self._map.nonnormalset:
-            self._map.nonnormalset.remove(f)
-        if mtime > self._lastnormaltime:
-            # Remember the most recent modification timeslot for status(),
-            # to make sure we won't miss future size-preserving file content
-            # modifications that happen within the same timeslot.
-            self._lastnormaltime = mtime
-
-    def normallookup(self, f):
-        '''Mark a file normal, but possibly dirty.'''
-        if self._pl[1] != self._nodeconstants.nullid:
-            # if there is a merge going on and the file was either
-            # in state 'm' (-1) or coming from other parent (-2) before
-            # being removed, restore that state.
-            entry = self._map.get(f)
-            if entry is not None:
-                if entry[0] == b'r' and entry[2] in (NONNORMAL, FROM_P2):
-                    source = self._map.copymap.get(f)
-                    if entry[2] == NONNORMAL:
-                        self.merge(f)
-                    elif entry[2] == FROM_P2:
-                        self.otherparent(f)
-                    if source:
-                        self.copy(source, f)
-                    return
-                if entry[0] == b'm' or entry[0] == b'n' and entry[2] == FROM_P2:
-                    return
-        self._addpath(f, b'n', 0, possibly_dirty=True)
-        self._map.copymap.pop(f, None)
-
-    def otherparent(self, f):
-        '''Mark as coming from the other parent, always dirty.'''
-        if self._pl[1] == self._nodeconstants.nullid:
-            msg = _(b"setting %r to other parent only allowed in merges") % f
-            raise error.Abort(msg)
-        if f in self and self[f] == b'n':
-            # merge-like
-            self._addpath(f, b'm', 0, from_p2=True)
-        else:
-            # add-like
-            self._addpath(f, b'n', 0, from_p2=True)
-        self._map.copymap.pop(f, None)
-
-    def add(self, f):
-        '''Mark a file added.'''
-        self._addpath(f, b'a', 0)
-        self._map.copymap.pop(f, None)
-
-    def remove(self, f):
-        '''Mark a file removed.'''
-        self._dirty = True
-        oldstate = self[f]
-        size = 0
-        if self._pl[1] != self._nodeconstants.nullid:
-            entry = self._map.get(f)
-            if entry is not None:
-                # backup the previous state
-                if entry[0] == b'm':  # merge
-                    size = NONNORMAL
-                elif entry[0] == b'n' and entry[2] == FROM_P2:  # other parent
-                    size = FROM_P2
-                    self._map.otherparentset.add(f)
-        self._updatedfiles.add(f)
-        self._map.removefile(f, oldstate, size)
-        if size == 0:
-            self._map.copymap.pop(f, None)
-
-    def merge(self, f):
-        '''Mark a file merged.'''
-        if self._pl[1] == self._nodeconstants.nullid:
-            return self.normallookup(f)
-        return self.otherparent(f)
-
-    def drop(self, f):
-        '''Drop a file from the dirstate'''
-        oldstate = self[f]
-        if self._map.dropfile(f, oldstate):
-            self._dirty = True
-            self._updatedfiles.add(f)
-            self._map.copymap.pop(f, None)
-
-    def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
-        if exists is None:
-            exists = os.path.lexists(os.path.join(self._root, path))
-        if not exists:
-            # Maybe a path component exists
-            if not ignoremissing and b'/' in path:
-                d, f = path.rsplit(b'/', 1)
-                d = self._normalize(d, False, ignoremissing, None)
-                folded = d + b"/" + f
-            else:
-                # No path components, preserve original case
-                folded = path
-        else:
-            # recursively normalize leading directory components
-            # against dirstate
-            if b'/' in normed:
-                d, f = normed.rsplit(b'/', 1)
-                d = self._normalize(d, False, ignoremissing, True)
-                r = self._root + b"/" + d
-                folded = d + b"/" + util.fspath(f, r)
-            else:
-                folded = util.fspath(normed, self._root)
-            storemap[normed] = folded
-
-        return folded
-
-    def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
-        normed = util.normcase(path)
-        folded = self._map.filefoldmap.get(normed, None)
-        if folded is None:
-            if isknown:
-                folded = path
-            else:
-                folded = self._discoverpath(
-                    path, normed, ignoremissing, exists, self._map.filefoldmap
-                )
-        return folded
-
-    def _normalize(self, path, isknown, ignoremissing=False, exists=None):
-        normed = util.normcase(path)
-        folded = self._map.filefoldmap.get(normed, None)
-        if folded is None:
-            folded = self._map.dirfoldmap.get(normed, None)
-        if folded is None:
-            if isknown:
-                folded = path
-            else:
-                # store discovered result in dirfoldmap so that future
-                # normalizefile calls don't start matching directories
-                folded = self._discoverpath(
-                    path, normed, ignoremissing, exists, self._map.dirfoldmap
-                )
-        return folded
-
-    def normalize(self, path, isknown=False, ignoremissing=False):
-        """
-        normalize the case of a pathname when on a casefolding filesystem
-
-        isknown specifies whether the filename came from walking the
-        disk, to avoid extra filesystem access.
-
-        If ignoremissing is True, missing path are returned
-        unchanged. Otherwise, we try harder to normalize possibly
-        existing path components.
-
-        The normalized case is determined based on the following precedence:
-
-        - version of name already stored in the dirstate
-        - version of name stored on disk
-        - version provided via command arguments
-        """
-
-        if self._checkcase:
-            return self._normalize(path, isknown, ignoremissing)
-        return path
-
-    def clear(self):
-        self._map.clear()
-        self._lastnormaltime = 0
-        self._updatedfiles.clear()
-        self._dirty = True
-
-    def rebuild(self, parent, allfiles, changedfiles=None):
-        if changedfiles is None:
-            # Rebuild entire dirstate
-            to_lookup = allfiles
-            to_drop = []
-            lastnormaltime = self._lastnormaltime
-            self.clear()
-            self._lastnormaltime = lastnormaltime
-        elif len(changedfiles) < 10:
-            # Avoid turning allfiles into a set, which can be expensive if it's
-            # large.
-            to_lookup = []
-            to_drop = []
-            for f in changedfiles:
-                if f in allfiles:
-                    to_lookup.append(f)
-                else:
-                    to_drop.append(f)
-        else:
-            changedfilesset = set(changedfiles)
-            to_lookup = changedfilesset & set(allfiles)
-            to_drop = changedfilesset - to_lookup
-
-        if self._origpl is None:
-            self._origpl = self._pl
-        self._map.setparents(parent, self._nodeconstants.nullid)
-
-        for f in to_lookup:
-            self.normallookup(f)
-        for f in to_drop:
-            self.drop(f)
-
-        self._dirty = True
-
-    def identity(self):
-        """Return identity of dirstate itself to detect changing in storage
-
-        If identity of previous dirstate is equal to this, writing
-        changes based on the former dirstate out can keep consistency.
-        """
-        return self._map.identity
-
-    def write(self, tr):
-        if not self._dirty:
-            return
-
-        filename = self._filename
-        if tr:
-            # 'dirstate.write()' is not only for writing in-memory
-            # changes out, but also for dropping ambiguous timestamp.
-            # delayed writing re-raise "ambiguous timestamp issue".
-            # See also the wiki page below for detail:
-            # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
-
-            # emulate dropping timestamp in 'parsers.pack_dirstate'
-            now = _getfsnow(self._opener)
-            self._map.clearambiguoustimes(self._updatedfiles, now)
-
-            # emulate that all 'dirstate.normal' results are written out
-            self._lastnormaltime = 0
-            self._updatedfiles.clear()
-
-            # delay writing in-memory changes out
-            tr.addfilegenerator(
-                b'dirstate',
-                (self._filename,),
-                self._writedirstate,
-                location=b'plain',
-            )
-            return
-
-        st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
-        self._writedirstate(st)
-
-    def addparentchangecallback(self, category, callback):
-        """add a callback to be called when the wd parents are changed
-
-        Callback will be called with the following arguments:
-            dirstate, (oldp1, oldp2), (newp1, newp2)
-
-        Category is a unique identifier to allow overwriting an old callback
-        with a newer callback.
-        """
-        self._plchangecallbacks[category] = callback
-
-    def _writedirstate(self, st):
-        # notify callbacks about parents change
-        if self._origpl is not None and self._origpl != self._pl:
-            for c, callback in sorted(
-                pycompat.iteritems(self._plchangecallbacks)
-            ):
-                callback(self, self._origpl, self._pl)
-            self._origpl = None
-        # use the modification time of the newly created temporary file as the
-        # filesystem's notion of 'now'
-        now = util.fstat(st)[stat.ST_MTIME] & _rangemask
-
-        # enough 'delaywrite' prevents 'pack_dirstate' from dropping
-        # timestamp of each entries in dirstate, because of 'now > mtime'
-        delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
-        if delaywrite > 0:
-            # do we have any files to delay for?
-            for f, e in pycompat.iteritems(self._map):
-                if e[0] == b'n' and e[3] == now:
-                    import time  # to avoid useless import
-
-                    # rather than sleep n seconds, sleep until the next
-                    # multiple of n seconds
-                    clock = time.time()
-                    start = int(clock) - (int(clock) % delaywrite)
-                    end = start + delaywrite
-                    time.sleep(end - clock)
-                    now = end  # trust our estimate that the end is near now
-                    break
-
-        self._map.write(st, now)
-        self._lastnormaltime = 0
-        self._dirty = False
-
-    def _dirignore(self, f):
-        if self._ignore(f):
-            return True
-        for p in pathutil.finddirs(f):
-            if self._ignore(p):
-                return True
-        return False
-
-    def _ignorefiles(self):
-        files = []
-        if os.path.exists(self._join(b'.hgignore')):
-            files.append(self._join(b'.hgignore'))
-        for name, path in self._ui.configitems(b"ui"):
-            if name == b'ignore' or name.startswith(b'ignore.'):
-                # we need to use os.path.join here rather than self._join
-                # because path is arbitrary and user-specified
-                files.append(os.path.join(self._rootdir, util.expandpath(path)))
-        return files
-
-    def _ignorefileandline(self, f):
-        files = collections.deque(self._ignorefiles())
-        visited = set()
-        while files:
-            i = files.popleft()
-            patterns = matchmod.readpatternfile(
-                i, self._ui.warn, sourceinfo=True
-            )
-            for pattern, lineno, line in patterns:
-                kind, p = matchmod._patsplit(pattern, b'glob')
-                if kind == b"subinclude":
-                    if p not in visited:
-                        files.append(p)
-                    continue
-                m = matchmod.match(
-                    self._root, b'', [], [pattern], warn=self._ui.warn
-                )
-                if m(f):
-                    return (i, lineno, line)
-            visited.add(i)
-        return (None, -1, b"")
-
-    def _walkexplicit(self, match, subrepos):
-        """Get stat data about the files explicitly specified by match.
-
-        Return a triple (results, dirsfound, dirsnotfound).
-        - results is a mapping from filename to stat result. It also contains
-          listings mapping subrepos and .hg to None.
-        - dirsfound is a list of files found to be directories.
-        - dirsnotfound is a list of files that the dirstate thinks are
-          directories and that were not found."""
-
-        def badtype(mode):
-            kind = _(b'unknown')
-            if stat.S_ISCHR(mode):
-                kind = _(b'character device')
-            elif stat.S_ISBLK(mode):
-                kind = _(b'block device')
-            elif stat.S_ISFIFO(mode):
-                kind = _(b'fifo')
-            elif stat.S_ISSOCK(mode):
-                kind = _(b'socket')
-            elif stat.S_ISDIR(mode):
-                kind = _(b'directory')
-            return _(b'unsupported file type (type is %s)') % kind
-
-        badfn = match.bad
-        dmap = self._map
-        lstat = os.lstat
-        getkind = stat.S_IFMT
-        dirkind = stat.S_IFDIR
-        regkind = stat.S_IFREG
-        lnkkind = stat.S_IFLNK
-        join = self._join
-        dirsfound = []
-        foundadd = dirsfound.append
-        dirsnotfound = []
-        notfoundadd = dirsnotfound.append
-
-        if not match.isexact() and self._checkcase:
-            normalize = self._normalize
-        else:
-            normalize = None
-
-        files = sorted(match.files())
-        subrepos.sort()
-        i, j = 0, 0
-        while i < len(files) and j < len(subrepos):
-            subpath = subrepos[j] + b"/"
-            if files[i] < subpath:
-                i += 1
-                continue
-            while i < len(files) and files[i].startswith(subpath):
-                del files[i]
-            j += 1
-
-        if not files or b'' in files:
-            files = [b'']
-            # constructing the foldmap is expensive, so don't do it for the
-            # common case where files is ['']
-            normalize = None
-        results = dict.fromkeys(subrepos)
-        results[b'.hg'] = None
-
-        for ff in files:
-            if normalize:
-                nf = normalize(ff, False, True)
-            else:
-                nf = ff
-            if nf in results:
-                continue
-
-            try:
-                st = lstat(join(nf))
-                kind = getkind(st.st_mode)
-                if kind == dirkind:
-                    if nf in dmap:
-                        # file replaced by dir on disk but still in dirstate
-                        results[nf] = None
-                    foundadd((nf, ff))
-                elif kind == regkind or kind == lnkkind:
-                    results[nf] = st
-                else:
-                    badfn(ff, badtype(kind))
-                    if nf in dmap:
-                        results[nf] = None
-            except OSError as inst:  # nf not found on disk - it is dirstate only
-                if nf in dmap:  # does it exactly match a missing file?
-                    results[nf] = None
-                else:  # does it match a missing directory?
-                    if self._map.hasdir(nf):
-                        notfoundadd(nf)
-                    else:
-                        badfn(ff, encoding.strtolocal(inst.strerror))
-
-        # match.files() may contain explicitly-specified paths that shouldn't
-        # be taken; drop them from the list of files found. dirsfound/notfound
-        # aren't filtered here because they will be tested later.
-        if match.anypats():
-            for f in list(results):
-                if f == b'.hg' or f in subrepos:
-                    # keep sentinel to disable further out-of-repo walks
-                    continue
-                if not match(f):
-                    del results[f]
-
-        # Case insensitive filesystems cannot rely on lstat() failing to detect
-        # a case-only rename.  Prune the stat object for any file that does not
-        # match the case in the filesystem, if there are multiple files that
-        # normalize to the same path.
-        if match.isexact() and self._checkcase:
-            normed = {}
-
-            for f, st in pycompat.iteritems(results):
-                if st is None:
-                    continue
-
-                nc = util.normcase(f)
-                paths = normed.get(nc)
-
-                if paths is None:
-                    paths = set()
-                    normed[nc] = paths
-
-                paths.add(f)
-
-            for norm, paths in pycompat.iteritems(normed):
-                if len(paths) > 1:
-                    for path in paths:
-                        folded = self._discoverpath(
-                            path, norm, True, None, self._map.dirfoldmap
-                        )
-                        if path != folded:
-                            results[path] = None
-
-        return results, dirsfound, dirsnotfound
-
-    def walk(self, match, subrepos, unknown, ignored, full=True):
-        """
-        Walk recursively through the directory tree, finding all files
-        matched by match.
-
-        If full is False, maybe skip some known-clean files.
-
-        Return a dict mapping filename to stat-like object (either
-        mercurial.osutil.stat instance or return value of os.stat()).
-
-        """
-        # full is a flag that extensions that hook into walk can use -- this
-        # implementation doesn't use it at all. This satisfies the contract
-        # because we only guarantee a "maybe".
-
-        if ignored:
-            ignore = util.never
-            dirignore = util.never
-        elif unknown:
-            ignore = self._ignore
-            dirignore = self._dirignore
-        else:
-            # if not unknown and not ignored, drop dir recursion and step 2
-            ignore = util.always
-            dirignore = util.always
-
-        matchfn = match.matchfn
-        matchalways = match.always()
-        matchtdir = match.traversedir
-        dmap = self._map
-        listdir = util.listdir
-        lstat = os.lstat
-        dirkind = stat.S_IFDIR
-        regkind = stat.S_IFREG
-        lnkkind = stat.S_IFLNK
-        join = self._join
-
-        exact = skipstep3 = False
-        if match.isexact():  # match.exact
-            exact = True
-            dirignore = util.always  # skip step 2
-        elif match.prefix():  # match.match, no patterns
-            skipstep3 = True
-
-        if not exact and self._checkcase:
-            normalize = self._normalize
-            normalizefile = self._normalizefile
-            skipstep3 = False
-        else:
-            normalize = self._normalize
-            normalizefile = None
-
-        # step 1: find all explicit files
-        results, work, dirsnotfound = self._walkexplicit(match, subrepos)
-        if matchtdir:
-            for d in work:
-                matchtdir(d[0])
-            for d in dirsnotfound:
-                matchtdir(d)
-
-        skipstep3 = skipstep3 and not (work or dirsnotfound)
-        work = [d for d in work if not dirignore(d[0])]
-
-        # step 2: visit subdirectories
-        def traverse(work, alreadynormed):
-            wadd = work.append
-            while work:
-                tracing.counter('dirstate.walk work', len(work))
-                nd = work.pop()
-                visitentries = match.visitchildrenset(nd)
-                if not visitentries:
-                    continue
-                if visitentries == b'this' or visitentries == b'all':
-                    visitentries = None
-                skip = None
-                if nd != b'':
-                    skip = b'.hg'
-                try:
-                    with tracing.log('dirstate.walk.traverse listdir %s', nd):
-                        entries = listdir(join(nd), stat=True, skip=skip)
-                except OSError as inst:
-                    if inst.errno in (errno.EACCES, errno.ENOENT):
-                        match.bad(
-                            self.pathto(nd), encoding.strtolocal(inst.strerror)
-                        )
-                        continue
-                    raise
-                for f, kind, st in entries:
-                    # Some matchers may return files in the visitentries set,
-                    # instead of 'this', if the matcher explicitly mentions them
-                    # and is not an exactmatcher. This is acceptable; we do not
-                    # make any hard assumptions about file-or-directory below
-                    # based on the presence of `f` in visitentries. If
-                    # visitchildrenset returned a set, we can always skip the
-                    # entries *not* in the set it provided regardless of whether
-                    # they're actually a file or a directory.
-                    if visitentries and f not in visitentries:
-                        continue
-                    if normalizefile:
-                        # even though f might be a directory, we're only
-                        # interested in comparing it to files currently in the
-                        # dmap -- therefore normalizefile is enough
-                        nf = normalizefile(
-                            nd and (nd + b"/" + f) or f, True, True
-                        )
-                    else:
-                        nf = nd and (nd + b"/" + f) or f
-                    if nf not in results:
-                        if kind == dirkind:
-                            if not ignore(nf):
-                                if matchtdir:
-                                    matchtdir(nf)
-                                wadd(nf)
-                            if nf in dmap and (matchalways or matchfn(nf)):
-                                results[nf] = None
-                        elif kind == regkind or kind == lnkkind:
-                            if nf in dmap:
-                                if matchalways or matchfn(nf):
-                                    results[nf] = st
-                            elif (matchalways or matchfn(nf)) and not ignore(
-                                nf
-                            ):
-                                # unknown file -- normalize if necessary
-                                if not alreadynormed:
-                                    nf = normalize(nf, False, True)
-                                results[nf] = st
-                        elif nf in dmap and (matchalways or matchfn(nf)):
-                            results[nf] = None
-
-        for nd, d in work:
-            # alreadynormed means that processwork doesn't have to do any
-            # expensive directory normalization
-            alreadynormed = not normalize or nd == d
-            traverse([d], alreadynormed)
-
-        for s in subrepos:
-            del results[s]
-        del results[b'.hg']
-
-        # step 3: visit remaining files from dmap
-        if not skipstep3 and not exact:
-            # If a dmap file is not in results yet, it was either
-            # a) not matching matchfn b) ignored, c) missing, or d) under a
-            # symlink directory.
-            if not results and matchalways:
-                visit = [f for f in dmap]
-            else:
-                visit = [f for f in dmap if f not in results and matchfn(f)]
-            visit.sort()
-
-            if unknown:
-                # unknown == True means we walked all dirs under the roots
-                # that wasn't ignored, and everything that matched was stat'ed
-                # and is already in results.
-                # The rest must thus be ignored or under a symlink.
-                audit_path = pathutil.pathauditor(self._root, cached=True)
-
-                for nf in iter(visit):
-                    # If a stat for the same file was already added with a
-                    # different case, don't add one for this, since that would
-                    # make it appear as if the file exists under both names
-                    # on disk.
-                    if (
-                        normalizefile
-                        and normalizefile(nf, True, True) in results
-                    ):
-                        results[nf] = None
-                    # Report ignored items in the dmap as long as they are not
-                    # under a symlink directory.
-                    elif audit_path.check(nf):
-                        try:
-                            results[nf] = lstat(join(nf))
-                            # file was just ignored, no links, and exists
-                        except OSError:
-                            # file doesn't exist
-                            results[nf] = None
-                    else:
-                        # It's either missing or under a symlink directory
-                        # which we in this case report as missing
-                        results[nf] = None
-            else:
-                # We may not have walked the full directory tree above,
-                # so stat and check everything we missed.
-                iv = iter(visit)
-                for st in util.statfiles([join(i) for i in visit]):
-                    results[next(iv)] = st
-        return results
-
-    def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
-        # Force Rayon (Rust parallelism library) to respect the number of
-        # workers. This is a temporary workaround until Rust code knows
-        # how to read the config file.
-        numcpus = self._ui.configint(b"worker", b"numcpus")
-        if numcpus is not None:
-            encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
-
-        workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
-        if not workers_enabled:
-            encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
-
-        (
-            lookup,
-            modified,
-            added,
-            removed,
-            deleted,
-            clean,
-            ignored,
-            unknown,
-            warnings,
-            bad,
-            traversed,
-            dirty,
-        ) = rustmod.status(
-            self._map._rustmap,
-            matcher,
-            self._rootdir,
-            self._ignorefiles(),
-            self._checkexec,
-            self._lastnormaltime,
-            bool(list_clean),
-            bool(list_ignored),
-            bool(list_unknown),
-            bool(matcher.traversedir),
-        )
-
-        self._dirty |= dirty
-
-        if matcher.traversedir:
-            for dir in traversed:
-                matcher.traversedir(dir)
-
-        if self._ui.warn:
-            for item in warnings:
-                if isinstance(item, tuple):
-                    file_path, syntax = item
-                    msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
-                        file_path,
-                        syntax,
-                    )
-                    self._ui.warn(msg)
-                else:
-                    msg = _(b"skipping unreadable pattern file '%s': %s\n")
-                    self._ui.warn(
-                        msg
-                        % (
-                            pathutil.canonpath(
-                                self._rootdir, self._rootdir, item
-                            ),
-                            b"No such file or directory",
-                        )
-                    )
-
-        for (fn, message) in bad:
-            matcher.bad(fn, encoding.strtolocal(message))
-
-        status = scmutil.status(
-            modified=modified,
-            added=added,
-            removed=removed,
-            deleted=deleted,
-            unknown=unknown,
-            ignored=ignored,
-            clean=clean,
-        )
-        return (lookup, status)
-
-    def status(self, match, subrepos, ignored, clean, unknown):
-        """Determine the status of the working copy relative to the
-        dirstate and return a pair of (unsure, status), where status is of type
-        scmutil.status and:
-
-          unsure:
-            files that might have been modified since the dirstate was
-            written, but need to be read to be sure (size is the same
-            but mtime differs)
-          status.modified:
-            files that have definitely been modified since the dirstate
-            was written (different size or mode)
-          status.clean:
-            files that have definitely not been modified since the
-            dirstate was written
-        """
-        listignored, listclean, listunknown = ignored, clean, unknown
-        lookup, modified, added, unknown, ignored = [], [], [], [], []
-        removed, deleted, clean = [], [], []
-
-        dmap = self._map
-        dmap.preload()
-
-        use_rust = True
-
-        allowed_matchers = (
-            matchmod.alwaysmatcher,
-            matchmod.exactmatcher,
-            matchmod.includematcher,
-        )
-
-        if rustmod is None:
-            use_rust = False
-        elif self._checkcase:
-            # Case-insensitive filesystems are not handled yet
-            use_rust = False
-        elif subrepos:
-            use_rust = False
-        elif sparse.enabled:
-            use_rust = False
-        elif not isinstance(match, allowed_matchers):
-            # Some matchers have yet to be implemented
-            use_rust = False
-
-        if use_rust:
-            try:
-                return self._rust_status(
-                    match, listclean, listignored, listunknown
-                )
-            except rustmod.FallbackError:
-                pass
-
-        def noop(f):
-            pass
-
-        dcontains = dmap.__contains__
-        dget = dmap.__getitem__
-        ladd = lookup.append  # aka "unsure"
-        madd = modified.append
-        aadd = added.append
-        uadd = unknown.append if listunknown else noop
-        iadd = ignored.append if listignored else noop
-        radd = removed.append
-        dadd = deleted.append
-        cadd = clean.append if listclean else noop
-        mexact = match.exact
-        dirignore = self._dirignore
-        checkexec = self._checkexec
-        copymap = self._map.copymap
-        lastnormaltime = self._lastnormaltime
-
-        # We need to do full walks when either
-        # - we're listing all clean files, or
-        # - match.traversedir does something, because match.traversedir should
-        #   be called for every dir in the working dir
-        full = listclean or match.traversedir is not None
-        for fn, st in pycompat.iteritems(
-            self.walk(match, subrepos, listunknown, listignored, full=full)
-        ):
-            if not dcontains(fn):
-                if (listignored or mexact(fn)) and dirignore(fn):
-                    if listignored:
-                        iadd(fn)
-                else:
-                    uadd(fn)
-                continue
-
-            # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
-            # written like that for performance reasons. dmap[fn] is not a
-            # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
-            # opcode has fast paths when the value to be unpacked is a tuple or
-            # a list, but falls back to creating a full-fledged iterator in
-            # general. That is much slower than simply accessing and storing the
-            # tuple members one by one.
-            t = dget(fn)
-            state = t[0]
-            mode = t[1]
-            size = t[2]
-            time = t[3]
-
-            if not st and state in b"nma":
-                dadd(fn)
-            elif state == b'n':
-                if (
-                    size >= 0
-                    and (
-                        (size != st.st_size and size != st.st_size & _rangemask)
-                        or ((mode ^ st.st_mode) & 0o100 and checkexec)
-                    )
-                    or size == FROM_P2  # other parent
-                    or fn in copymap
-                ):
-                    if stat.S_ISLNK(st.st_mode) and size != st.st_size:
-                        # issue6456: Size returned may be longer due to
-                        # encryption on EXT-4 fscrypt, undecided.
-                        ladd(fn)
-                    else:
-                        madd(fn)
-                elif (
-                    time != st[stat.ST_MTIME]
-                    and time != st[stat.ST_MTIME] & _rangemask
-                ):
-                    ladd(fn)
-                elif st[stat.ST_MTIME] == lastnormaltime:
-                    # fn may have just been marked as normal and it may have
-                    # changed in the same second without changing its size.
-                    # This can happen if we quickly do multiple commits.
-                    # Force lookup, so we don't miss such a racy file change.
-                    ladd(fn)
-                elif listclean:
-                    cadd(fn)
-            elif state == b'm':
-                madd(fn)
-            elif state == b'a':
-                aadd(fn)
-            elif state == b'r':
-                radd(fn)
-        status = scmutil.status(
-            modified, added, removed, deleted, unknown, ignored, clean
-        )
-        return (lookup, status)
-
-    def matches(self, match):
-        """
-        return files in the dirstate (in whatever state) filtered by match
-        """
-        dmap = self._map
-        if rustmod is not None:
-            dmap = self._map._rustmap
-
-        if match.always():
-            return dmap.keys()
-        files = match.files()
-        if match.isexact():
-            # fast path -- filter the other way around, since typically files is
-            # much smaller than dmap
-            return [f for f in files if f in dmap]
-        if match.prefix() and all(fn in dmap for fn in files):
-            # fast path -- all the values are known to be files, so just return
-            # that
-            return list(files)
-        return [f for f in dmap if match(f)]
-
-    def _actualfilename(self, tr):
-        if tr:
-            return self._pendingfilename
-        else:
-            return self._filename
-
-    def savebackup(self, tr, backupname):
-        '''Save current dirstate into backup file'''
-        filename = self._actualfilename(tr)
-        assert backupname != filename
-
-        # use '_writedirstate' instead of 'write' to write changes certainly,
-        # because the latter omits writing out if transaction is running.
-        # output file will be used to create backup of dirstate at this point.
-        if self._dirty or not self._opener.exists(filename):
-            self._writedirstate(
-                self._opener(filename, b"w", atomictemp=True, checkambig=True)
-            )
-
-        if tr:
-            # ensure that subsequent tr.writepending returns True for
-            # changes written out above, even if dirstate is never
-            # changed after this
-            tr.addfilegenerator(
-                b'dirstate',
-                (self._filename,),
-                self._writedirstate,
-                location=b'plain',
-            )
-
-            # ensure that pending file written above is unlinked at
-            # failure, even if tr.writepending isn't invoked until the
-            # end of this transaction
-            tr.registertmp(filename, location=b'plain')
-
-        self._opener.tryunlink(backupname)
-        # hardlink backup is okay because _writedirstate is always called
-        # with an "atomictemp=True" file.
-        util.copyfile(
-            self._opener.join(filename),
-            self._opener.join(backupname),
-            hardlink=True,
-        )
-
-    def restorebackup(self, tr, backupname):
-        '''Restore dirstate by backup file'''
-        # this "invalidate()" prevents "wlock.release()" from writing
-        # changes of dirstate out after restoring from backup file
-        self.invalidate()
-        filename = self._actualfilename(tr)
-        o = self._opener
-        if util.samefile(o.join(backupname), o.join(filename)):
-            o.unlink(backupname)
-        else:
-            o.rename(backupname, filename, checkambig=True)
-
-    def clearbackup(self, tr, backupname):
-        '''Clear backup file'''
-        self._opener.unlink(backupname)
-
-
 class dirstatemap(object):
     """Map encapsulating the dirstate's contents.
 
diff --git a/tests/fakedirstatewritetime.py b/tests/fakedirstatewritetime.py
--- a/tests/fakedirstatewritetime.py
+++ b/tests/fakedirstatewritetime.py
@@ -10,6 +10,7 @@
 from mercurial import (
     context,
     dirstate,
+    dirstatemap as dirstatemapmod,
     extensions,
     policy,
     registrar,
@@ -66,11 +67,11 @@
     if rustmod is not None:
         # The Rust implementation does not use public parse/pack dirstate
         # to prevent conversion round-trips
-        orig_dirstatemap_write = dirstate.dirstatemap.write
+        orig_dirstatemap_write = dirstatemapmod.dirstatemap.write
         wrapper = lambda self, st, now: orig_dirstatemap_write(
             self, st, fakenow
         )
-        dirstate.dirstatemap.write = wrapper
+        dirstatemapmod.dirstatemap.write = wrapper
 
     orig_dirstate_getfsnow = dirstate._getfsnow
     wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args)
@@ -86,7 +87,7 @@
         orig_module.pack_dirstate = orig_pack_dirstate
         dirstate._getfsnow = orig_dirstate_getfsnow
         if rustmod is not None:
-            dirstate.dirstatemap.write = orig_dirstatemap_write
+            dirstatemapmod.dirstatemap.write = orig_dirstatemap_write
 
 
 def _poststatusfixup(orig, workingctx, status, fixup):