diff --git a/tests/blacklist-treedirstate b/tests/blacklist-treedirstate
new file mode 100644
--- /dev/null
+++ b/tests/blacklist-treedirstate
@@ -0,0 +1,46 @@
+# These tests do not work with treedirstate overriding the dirstate
+# implementation.
+#
+# To run the core hg tests with treedirstate, use:
+#   python run-tests.py \
+#       --extra-config extensions.treedirstate=<path to treedirstate.py> \
+#       --extra-config treedirstate.useinnewrepos=True \
+#       --blacklist <path to blacklist-treedirstate>
+
+# These tests do not support the existence of an extra extension or new entries
+# in repo requirements.
+test-basic.t
+test-blackbox.t
+test-command-template.t
+test-commandserver.t
+test-completion.t
+test-debugextensions.t
+test-devel-warnings.t
+test-extension.t
+test-fncache.t
+test-globalopts.t
+test-hardlinks.t
+test-help.t
+test-hgrc.t
+test-inherit-mode.t
+test-init.t
+test-keyword.t
+test-lfconvert.t
+test-repo-compengines.t
+test-revlog-v2.t
+test-run-tests.t
+test-sparse-requirement.t
+test-static-http.t
+test-upgrade-repo.t
+test-wireproto.t
+
+# These tests monkey-patch or modify internal state of the core dirstate
+# implementation.
+test-merge-tools.t
+test-merge1.t
+test-revert.t
+test-subrepo.t
+test-contrib-perf.t
+
+# These tests rely on specific behaviour of the core dirstate implementation.
+test-dirstate-backup.t
diff --git a/tests/check-ext.py b/tests/check-ext.py
--- a/tests/check-ext.py
+++ b/tests/check-ext.py
@@ -17,6 +17,9 @@
 foreignexts = set(['remotenames'])
 foreignextre = re.compile(r'(%s)' % '|'.join(foreignexts))
 
+# pattern for rust extensions
+rustextre = re.compile(r'^hgext3rd\.rust\..*')
+
 # extensions in this repo
 repoexts = set(os.path.basename(p).split('.')[0]
                for p in glob('hgext3rd/*.py') if '__' not in p)
@@ -58,6 +61,8 @@
                 requiredexts = set(m.group(1).split())
                 unknownexts = requiredexts - foreignexts
                 for e in unknownexts:
+                    if rustextre.match(e):
+                        continue
                     if e in repoexts:
                         msg = 'do not require non-foreign extension %s'
                     else:
diff --git a/tests/test-treedirstate-backup.t b/tests/test-treedirstate-backup.t
new file mode 100644
--- /dev/null
+++ b/tests/test-treedirstate-backup.t
@@ -0,0 +1,30 @@
+  $ . $TESTDIR/require-ext.sh hgext3rd.rust.treedirstate
+
+Copy of test-dirstate-backup.t for treedirstate
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > treedirstate=$TESTDIR/../hgext3rd/treedirstate.py
+  > [treedirstate]
+  > useinnewrepos=True
+  > EOF
+
+Set up
+
+  $ hg init repo
+  $ cd repo
+
+Try to import an empty patch
+
+  $ hg import --no-commit - <<EOF
+  > EOF
+  applying patch from stdin
+  abort: stdin: no diffs found
+  [255]
+
+No dirstate backups are left behind
+
+  $ ls .hg/dirstate* | sort
+  .hg/dirstate
+  .hg/dirstate.tree.* (glob)
+
diff --git a/tests/test-treedirstate-core.t b/tests/test-treedirstate-core.t
new file mode 100644
--- /dev/null
+++ b/tests/test-treedirstate-core.t
@@ -0,0 +1,104 @@
+  $ . $TESTDIR/require-ext.sh hgext3rd.rust.treedirstate
+
+Copy of test-dirstate.t for treedirstate
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > treedirstate=$TESTDIR/../hgext3rd/treedirstate.py
+  > [treedirstate]
+  > useinnewrepos=True
+  > EOF
+
+------ Test dirstate._dirs refcounting
+
+  $ hg init t
+  $ cd t
+  $ mkdir -p a/b/c/d
+  $ touch a/b/c/d/x
+  $ touch a/b/c/d/y
+  $ touch a/b/c/d/z
+  $ hg ci -Am m
+  adding a/b/c/d/x
+  adding a/b/c/d/y
+  adding a/b/c/d/z
+  $ hg mv a z
+  moving a/b/c/d/x to z/b/c/d/x (glob)
+  moving a/b/c/d/y to z/b/c/d/y (glob)
+  moving a/b/c/d/z to z/b/c/d/z (glob)
+
+Test name collisions
+
+  $ rm z/b/c/d/x
+  $ mkdir z/b/c/d/x
+  $ touch z/b/c/d/x/y
+  $ hg add z/b/c/d/x/y
+  abort: file 'z/b/c/d/x' in dirstate clashes with 'z/b/c/d/x/y'
+  [255]
+  $ rm -rf z/b/c/d
+  $ touch z/b/c/d
+  $ hg add z/b/c/d
+  abort: directory 'z/b/c/d' already in dirstate
+  [255]
+
+  $ cd ..
+
+Issue1790: dirstate entry locked into unset if file mtime is set into
+the future
+
+Prepare test repo:
+
+  $ hg init u
+  $ cd u
+  $ echo a > a
+  $ hg add
+  adding a
+  $ hg ci -m1
+
+Set mtime of a into the future:
+
+  $ touch -t 202101011200 a
+
+Status must not set a's entry to unset (issue1790):
+
+  $ hg status
+  $ hg debugstate
+  n 644          2 2021-01-01 12:00:00 a
+
+Test modulo storage/comparison of absurd dates:
+
+#if no-aix
+  $ touch -t 195001011200 a
+  $ hg st
+  $ hg debugstate
+  n 644          2 2018-01-19 15:14:08 a
+#endif
+
+Verify that exceptions during a dirstate change leave the dirstate
+coherent (issue4353)
+
+  $ cat > ../dirstateexception.py <<EOF
+  > from __future__ import absolute_import
+  > from mercurial import (
+  >   error,
+  >   extensions,
+  >   merge,
+  > )
+  > 
+  > def wraprecordupdates(orig, repo, actions, branchmerge):
+  >     raise error.Abort("simulated error while recording dirstateupdates")
+  > 
+  > def reposetup(ui, repo):
+  >     extensions.wrapfunction(merge, 'recordupdates', wraprecordupdates)
+  > EOF
+
+  $ hg rm a
+  $ hg commit -m 'rm a'
+  $ echo "[extensions]" >> .hg/hgrc
+  $ echo "dirstateex=../dirstateexception.py" >> .hg/hgrc
+  $ hg up 0
+  abort: simulated error while recording dirstateupdates
+  [255]
+  $ hg log -r . -T '{rev}\n'
+  1
+  $ hg status
+  ? a
diff --git a/tests/test-treedirstate-nonnormalset.t b/tests/test-treedirstate-nonnormalset.t
new file mode 100644
--- /dev/null
+++ b/tests/test-treedirstate-nonnormalset.t
@@ -0,0 +1,33 @@
+  $ . $TESTDIR/require-ext.sh hgext3rd.rust.treedirstate
+
+Copy of test-dirstate-nonnormalsets.t for treedirstate
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > treedirstate=$TESTDIR/../hgext3rd/treedirstate.py
+  > [treedirstate]
+  > useinnewrepos=True
+  > EOF
+
+  $ cat >> $HGRCPATH << EOF
+  > [ui]
+  > logtemplate="{rev}:{node|short} ({phase}) [{tags} {bookmarks}] {desc|firstline}\n"
+  > [extensions]
+  > dirstateparanoidcheck = $RUNTESTDIR/../contrib/dirstatenonnormalcheck.py
+  > [experimental]
+  > nonnormalparanoidcheck = True
+  > [devel]
+  > all-warnings=True
+  > EOF
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+  $ hg init testrepo
+  $ cd testrepo
+  $ mkcommit a
+  $ mkcommit b
+  $ mkcommit c
+  $ hg status
diff --git a/tests/test-treedirstate-race.t b/tests/test-treedirstate-race.t
new file mode 100644
--- /dev/null
+++ b/tests/test-treedirstate-race.t
@@ -0,0 +1,249 @@
+  $ . $TESTDIR/require-ext.sh hgext3rd.rust.treedirstate
+
+Copy of core Mercurial test-dirstate-race, for treedirstate.
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > treedirstate=$TESTDIR/../hgext3rd/treedirstate.py
+  > [treedirstate]
+  > useinnewrepos=True
+  > EOF
+
+Setup
+
+  $ hg init repo
+  $ cd repo
+  $ echo a > a
+  $ hg add a
+  $ hg commit -m test
+
+Do we ever miss a sub-second change?:
+
+  $ for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
+  >     hg co -qC 0
+  >     echo b > a
+  >     hg st
+  > done
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+  M a
+
+  $ echo test > b
+  $ mkdir dir1
+  $ echo test > dir1/c
+  $ echo test > d
+
+  $ echo test > e
+#if execbit
+A directory will typically have the execute bit -- make sure it doesn't get
+confused with a file with the exec bit set
+  $ chmod +x e
+#endif
+
+  $ hg add b dir1 d e
+  adding dir1/c (glob)
+  $ hg commit -m test2
+
+  $ cat >> $TESTTMP/dirstaterace.py << EOF
+  > from mercurial import (
+  >     context,
+  >     extensions,
+  > )
+  > def extsetup():
+  >     extensions.wrapfunction(context.workingctx, '_checklookup', overridechecklookup)
+  > def overridechecklookup(orig, self, files):
+  >     # make an update that changes the dirstate from underneath
+  >     self._repo.ui.system(r"sh '$TESTTMP/dirstaterace.sh'",
+  >                          cwd=self._repo.root)
+  >     return orig(self, files)
+  > EOF
+
+  $ hg debugrebuilddirstate
+  $ hg debugdirstate
+  n   0         -1 unset               a
+  n   0         -1 unset               b
+  n   0         -1 unset               d
+  n   0         -1 unset               dir1/c
+  n   0         -1 unset               e
+
+XXX Note that this returns M for files that got replaced by directories. This is
+definitely a bug, but the fix for that is hard and the next status run is fine
+anyway.
+
+  $ cat > $TESTTMP/dirstaterace.sh <<EOF
+  > rm b && rm -r dir1 && rm d && mkdir d && rm e && mkdir e
+  > EOF
+
+  $ hg status --config extensions.dirstaterace=$TESTTMP/dirstaterace.py
+  M d
+  M e
+  ! b
+  ! dir1/c
+  $ hg debugdirstate
+  n 644          2 * a (glob)
+  n   0         -1 unset               b
+  n   0         -1 unset               d
+  n   0         -1 unset               dir1/c
+  n   0         -1 unset               e
+
+  $ hg status
+  ! b
+  ! d
+  ! dir1/c
+  ! e
+
+  $ rmdir d e
+  $ hg update -C -q .
+
+Test that dirstate changes aren't written out at the end of "hg
+status", if .hg/dirstate is already changed simultaneously before
+acquisition of wlock in workingctx._poststatusfixup().
+
+This avoidance is important to keep consistency of dirstate in race
+condition (see issue5584 for detail).
+
+  $ hg parents -q
+  1:* (glob)
+
+  $ hg debugrebuilddirstate
+  $ hg debugdirstate
+  n   0         -1 unset               a
+  n   0         -1 unset               b
+  n   0         -1 unset               d
+  n   0         -1 unset               dir1/c
+  n   0         -1 unset               e
+
+  $ cat > $TESTTMP/dirstaterace.sh <<EOF
+  > # This script assumes timetable of typical issue5584 case below:
+  > #
+  > # 1. "hg status" loads .hg/dirstate
+  > # 2. "hg status" confirms clean-ness of FILE
+  > # 3. "hg update -C 0" updates the working directory simultaneously
+  > #    (FILE is removed, and FILE is dropped from .hg/dirstate)
+  > # 4. "hg status" acquires wlock
+  > #    (.hg/dirstate is re-loaded = no FILE entry in dirstate)
+  > # 5. "hg status" marks FILE in dirstate as clean
+  > #    (FILE entry is added to in-memory dirstate)
+  > # 6. "hg status" writes dirstate changes into .hg/dirstate
+  > #    (FILE entry is written into .hg/dirstate)
+  > #
+  > # To reproduce similar situation easily and certainly, #2 and #3
+  > # are swapped.  "hg cat" below ensures #2 on "hg status" side.
+  > 
+  > hg update -q -C 0
+  > hg cat -r 1 b > b
+  > EOF
+
+"hg status" below should excludes "e", of which exec flag is set, for
+portability of test scenario, because unsure but missing "e" is
+treated differently in _checklookup() according to runtime platform.
+
+- "missing(!)" on POSIX, "pctx[f].cmp(self[f])" raises ENOENT
+- "modified(M)" on Windows, "self.flags(f) != pctx.flags(f)" is True
+
+  $ hg status --config extensions.dirstaterace=$TESTTMP/dirstaterace.py --debug -X path:e
+  skip updating dirstate: identity mismatch
+  M a
+  ! d
+  ! dir1/c
+
+  $ hg parents -q
+  0:* (glob)
+  $ hg files
+  a
+  $ hg debugdirstate
+  n * * * a (glob)
+
+  $ rm b
+
+#if fsmonitor
+
+Create fsmonitor state.
+
+  $ hg status
+  $ f --type .hg/fsmonitor.state
+  .hg/fsmonitor.state: file
+
+Test that invalidating fsmonitor state in the middle (which doesn't require the
+wlock) causes the fsmonitor update to be skipped.
+hg debugrebuilddirstate ensures that the dirstaterace hook will be called, but
+it also invalidates the fsmonitor state. So back it up and restore it.
+
+  $ mv .hg/fsmonitor.state .hg/fsmonitor.state.tmp
+  $ hg debugrebuilddirstate
+  $ mv .hg/fsmonitor.state.tmp .hg/fsmonitor.state
+
+  $ cat > $TESTTMP/dirstaterace.sh <<EOF
+  > rm .hg/fsmonitor.state
+  > EOF
+
+  $ hg status --config extensions.dirstaterace=$TESTTMP/dirstaterace.py --debug
+  skip updating fsmonitor.state: identity mismatch
+  $ f .hg/fsmonitor.state
+  .hg/fsmonitor.state: file not found
+
+#endif
+
+Set up a rebase situation for issue5581.
+
+  $ echo c2 > a
+  $ echo c2 > b
+  $ hg add b
+  $ hg commit -m c2
+  created new head
+  $ echo c3 >> a
+  $ hg commit -m c3
+  $ hg update 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo c4 >> a
+  $ echo c4 >> b
+  $ hg commit -m c4
+  created new head
+
+Configure a merge tool that runs status in the middle of the rebase. The goal of
+the status call is to trigger a potential bug if fsmonitor's state is written
+even though the wlock is held by another process. The output of 'hg status' in
+the merge tool goes to /dev/null because we're more interested in the results of
+'hg status' run after the rebase.
+
+  $ cat >> $TESTTMP/mergetool-race.sh << EOF
+  > echo "custom merge tool"
+  > printf "c2\nc3\nc4\n" > \$1
+  > hg --cwd "$TESTTMP/repo" status > /dev/null
+  > echo "custom merge tool end"
+  > EOF
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > rebase =
+  > [merge-tools]
+  > test.executable=sh
+  > test.args=$TESTTMP/mergetool-race.sh \$output
+  > EOF
+
+  $ hg rebase -s . -d 3 --tool test
+  rebasing 4:b08445fd6b2a "c4" (tip)
+  merging a
+  custom merge tool
+  custom merge tool end
+  saved backup bundle to $TESTTMP/repo/.hg/strip-backup/* (glob)
+
+This hg status should be empty, whether or not fsmonitor is enabled (issue5581).
+
+  $ hg status
diff --git a/tests/test-treedirstate.t b/tests/test-treedirstate.t
new file mode 100644
--- /dev/null
+++ b/tests/test-treedirstate.t
@@ -0,0 +1,202 @@
+  $ . $TESTDIR/require-ext.sh hgext3rd.rust.treedirstate
+
+Setup
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > treedirstate=$TESTDIR/../hgext3rd/treedirstate.py
+  > [treedirstate]
+  > useinnewrepos=True
+  > EOF
+
+  $ hg init repo
+  $ cd repo
+  $ echo base > base
+  $ hg add base
+  $ hg debugdirstate
+  a   0         -1 unset               base
+  $ hg commit -m "base"
+  $ hg debugdirstate
+  n 644          5 * base (glob)
+
+Create path-conflicting dirstates
+
+  $ hg up -q 0
+  $ echo a > a
+  $ hg add a
+  $ hg commit -m a
+  $ hg bookmark a
+  $ hg up -q 0
+  $ mkdir a
+  $ echo a/a > a/a
+  $ hg add a/a
+  $ hg commit -m a/a
+  created new head
+  $ hg bookmark a/a
+  $ hg up -q a
+  $ hg status
+  $ hg rm a
+  $ hg status
+  R a
+  $ hg merge --force a/a
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg status
+  M a/a
+  R a
+  $ hg rm --force a/a
+  $ hg status
+  R a
+  R a/a
+  $ hg up -Cq 0
+
+Attempt to create a path conflict in the manifest
+
+  $ echo b > b
+  $ hg add b
+  $ hg commit -m b
+  created new head
+  $ rm b
+  $ mkdir b
+  $ echo b/b > b/b
+  $ hg add b/b
+  abort: file 'b' in dirstate clashes with 'b/b'
+  [255]
+  $ rm -rf b
+  $ hg up -Cq 0
+
+Test warning when creating files that might give a casefold collision
+
+#if no-icasefs
+  $ echo data > FiLeNaMe
+  $ hg add FiLeNaMe
+  $ echo data > FILENAME
+  $ hg add FILENAME
+  warning: possible case-folding collision for FILENAME
+  $ rm -f FiLeNaMe FILENAME
+  $ hg up -Cq 0
+#endif
+
+Test dirfoldmap and filefoldmap on case insensitive filesystems
+
+#if icasefs
+  $ mkdir -p dirA/dirB/dirC
+  $ echo file1 > dira/File1
+  $ echo file2 > dira/dirb/FILE2
+  $ echo file3 > dira/dirb/dirc/FiLe3
+  $ echo file4 > dira/dirb/dirc/file4
+  $ hg add DIRA
+  adding dirA/File1
+  adding dirA/dirB/FILE2
+  adding dirA/dirB/dirC/FiLe3
+  adding dirA/dirB/dirC/file4
+  $ hg status
+  A dirA/File1
+  A dirA/dirB/FILE2
+  A dirA/dirB/dirC/FiLe3
+  A dirA/dirB/dirC/file4
+  $ hg forget dira/DIRB
+  removing dirA/dirB/FILE2
+  removing dirA/dirB/dirC/FiLe3
+  removing dirA/dirB/dirC/file4
+  $ hg status
+  A dirA/File1
+  ? dirA/dirB/FILE2
+  ? dirA/dirB/dirC/FiLe3
+  ? dirA/dirB/dirC/file4
+  $ hg add dira/dirb/file2
+  $ hg status
+  A dirA/File1
+  A dirA/dirB/FILE2
+  ? dirA/dirB/dirC/FiLe3
+  ? dirA/dirB/dirC/file4
+  $ rm -rf dirA
+#endif
+
+Test autorepack
+
+  $ ls .hg/dirstate.tree.*
+  .hg/dirstate.tree.* (glob)
+  $ echo data > file
+
+After the first repack, the old trees are kept around by the transaction undo backups.
+  $ hg add file --config treedirstate.minrepackthreshold=1 --config treedirstate.repackfactor=0 --debug | grep -v 'in use by'
+  adding file
+  auto-repacking treedirstate
+
+After the second repack, the tree is replaced by a new tree and then deleted.
+  $ hg forget file --config treedirstate.minrepackthreshold=1 --config treedirstate.repackfactor=0 --debug | grep -v 'in use by'
+  removing file
+  auto-repacking treedirstate
+  dirstate tree * unused, deleting (glob)
+  $ ls .hg/dirstate.tree.*
+  .hg/dirstate.tree.* (glob)
+  .hg/dirstate.tree.* (glob)
+
+Test downgrade and upgrade on pull
+
+  $ for f in 1 2 3 4 5 ; do mkdir dir$f ; echo $f > dir$f/file$f ; hg add dir$f/file$f ; done
+  $ echo x > a
+  $ hg add a
+  $ hg commit -m "add files"
+  created new head
+  $ cd ..
+  $ hg clone repo clone
+  updating to branch default
+  7 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd clone
+  $ hg merge 1
+  merging a
+  warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ echo data > newfile
+  $ hg add newfile
+  $ hg rm dir3/file3
+  $ grep treedirstate .hg/requires
+  treedirstate
+  $ hg pull --config treedirstate.downgradeonpull=true
+  disabling treedirstate...
+  pulling from $TESTTMP/repo (glob)
+  searching for changes
+  no changes found
+  $ hg debugdirstate
+  m   0         -2 * a (glob)
+  n 644          5 * base (glob)
+  n 644          2 * dir1/file1 (glob)
+  n 644          2 * dir2/file2 (glob)
+  r   0          0 * dir3/file3 (glob)
+  n 644          2 * dir4/file4 (glob)
+  n 644          2 * dir5/file5 (glob)
+  a   0         -1 * newfile (glob)
+  $ grep treedirstate .hg/requires
+  [1]
+  $ hg pull --config treedirstate.upgradeonpull=true
+  migrating your repo to treedirstate which will make your hg commands faster...
+  pulling from $TESTTMP/repo (glob)
+  searching for changes
+  no changes found
+  $ hg debugdirstate
+  m   0         -2 * a (glob)
+  n 644          5 * base (glob)
+  n 644          2 * dir1/file1 (glob)
+  n 644          2 * dir2/file2 (glob)
+  r   0          0 * dir3/file3 (glob)
+  n 644          2 * dir4/file4 (glob)
+  n 644          2 * dir5/file5 (glob)
+  a   0         -1 * newfile (glob)
+  $ grep treedirstate .hg/requires
+  treedirstate
+  $ hg pull --config treedirstate.upgradeonpull=true
+  pulling from $TESTTMP/repo (glob)
+  searching for changes
+  no changes found
+
+Test cleaning up on normal commands
+
+  $ echo fakedirstatetree > .hg/dirstate.tree.fake
+  $ hg forget newfile --debug --config treedirstate.cleanuppercent=100 | grep fake
+  dirstate tree fake unused, deleting
+  $ test -f .hg/dirstate.tree.fake
+  [1]