changeset 49005:12adf8c695ed

merge: stable into default
author Raphaël Gomès <rgomes@octobus.net>
date Tue, 05 Apr 2022 11:09:03 +0200
parents 20c6c9e43397 (current diff) 9dcfd1d05e6e (diff)
children 82dbe3a2920d
files mercurial/dispatch.py rust/Cargo.lock rust/hg-core/Cargo.toml rust/hg-core/src/dirstate_tree/dirstate_map.rs rust/hg-core/src/repo.rs
diffstat 12 files changed, 513 insertions(+), 388 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/dispatch.py	Wed Mar 30 00:57:08 2022 -0400
+++ b/mercurial/dispatch.py	Tue Apr 05 11:09:03 2022 +0200
@@ -269,7 +269,7 @@
             ferr.write(inst.format())
             return -1
 
-        msg = _formatargs(req.args)
+        formattedargs = _formatargs(req.args)
         starttime = util.timer()
         ret = 1  # default of Python exit code on unhandled exception
         try:
@@ -308,7 +308,7 @@
             req.ui.log(
                 b"commandfinish",
                 b"%s exited %d after %0.2f seconds\n",
-                msg,
+                formattedargs,
                 return_code,
                 duration,
                 return_code=return_code,
--- a/rust/Cargo.lock	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/Cargo.lock	Tue Apr 05 11:09:03 2022 +0200
@@ -3,6 +3,12 @@
 version = 3
 
 [[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
+[[package]]
 name = "adler"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +30,12 @@
 ]
 
 [[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
+[[package]]
 name = "ansi_term"
 version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -210,12 +222,22 @@
 
 [[package]]
 name = "crossbeam-channel"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-channel"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.1",
 ]
 
 [[package]]
@@ -226,7 +248,7 @@
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.1",
 ]
 
 [[package]]
@@ -237,7 +259,7 @@
 dependencies = [
  "cfg-if 1.0.0",
  "const_fn",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.1",
  "lazy_static",
  "memoffset",
  "scopeguard",
@@ -245,6 +267,17 @@
 
 [[package]]
 name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg",
+ "cfg-if 0.1.10",
+ "lazy_static",
+]
+
+[[package]]
+name = "crossbeam-utils"
 version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
@@ -443,7 +476,7 @@
  "byteorder",
  "bytes-cast",
  "clap",
- "crossbeam-channel",
+ "crossbeam-channel 0.4.4",
  "derive_more",
  "flate2",
  "format-bytes",
@@ -455,7 +488,8 @@
  "libc",
  "log",
  "memmap2",
- "micro-timer",
+ "micro-timer 0.3.1",
+ "ouroboros",
  "pretty_assertions",
  "rand 0.8.5",
  "rand_distr",
@@ -464,7 +498,6 @@
  "regex",
  "same-file",
  "sha-1 0.10.0",
- "stable_deref_trait",
  "tempfile",
  "twox-hash",
  "zstd",
@@ -475,7 +508,7 @@
 version = "0.1.0"
 dependencies = [
  "cpython",
- "crossbeam-channel",
+ "crossbeam-channel 0.5.2",
  "env_logger",
  "hg-core",
  "libc",
@@ -588,6 +621,12 @@
 ]
 
 [[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
 name = "memchr"
 version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -595,9 +634,9 @@
 
 [[package]]
 name = "memmap2"
-version = "0.5.3"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f"
+checksum = "de5d3112c080d58ce560081baeaab7e1e864ca21795ddbf533d5b1842bb1ecf8"
 dependencies = [
  "libc",
  "stable_deref_trait",
@@ -614,16 +653,38 @@
 
 [[package]]
 name = "micro-timer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2620153e1d903d26b72b89f0e9c48d8c4756cba941c185461dddc234980c298c"
+dependencies = [
+ "micro-timer-macros 0.3.1",
+ "scopeguard",
+]
+
+[[package]]
+name = "micro-timer"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5de32cb59a062672560d6f0842c4aa7714727457b9fe2daf8987d995a176a405"
 dependencies = [
- "micro-timer-macros",
+ "micro-timer-macros 0.4.0",
  "scopeguard",
 ]
 
 [[package]]
 name = "micro-timer-macros"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28a3473e6abd6e9aab36aaeef32ad22ae0bd34e79f376643594c2b152ec1c5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "scopeguard",
+ "syn",
+]
+
+[[package]]
+name = "micro-timer-macros"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cee948b94700125b52dfb68dd17c19f6326696c1df57f92c05ee857463c93ba1"
@@ -681,6 +742,30 @@
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
+name = "ouroboros"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f31a3b678685b150cba82b702dcdc5e155893f63610cf388d30cd988d4ca2bf"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "084fd65d5dd8b3772edccb5ffd1e4b7eba43897ecd0f9401e330e8c542959408"
+dependencies = [
+ "Inflector",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "output_vt100"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -720,6 +805,30 @@
 ]
 
 [[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
 name = "proc-macro2"
 version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -864,9 +973,9 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
 dependencies = [
- "crossbeam-channel",
+ "crossbeam-channel 0.5.2",
  "crossbeam-deque",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.1",
  "lazy_static",
  "num_cpus",
 ]
@@ -920,7 +1029,7 @@
  "home",
  "lazy_static",
  "log",
- "micro-timer",
+ "micro-timer 0.4.0",
  "regex",
  "users",
 ]
--- a/rust/hg-core/Cargo.toml	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/Cargo.toml	Tue Apr 05 11:09:03 2022 +0200
@@ -18,8 +18,9 @@
 im-rc = "15.0.0"
 itertools = "0.10.3"
 lazy_static = "1.4.0"
-libc = "0.2.119"
-rand = "0.8.5"
+libc = "0.2"
+ouroboros = "0.15.0"
+rand = "0.8.4"
 rand_pcg = "0.3.1"
 rand_distr = "0.4.3"
 rayon = "1.5.1"
@@ -27,12 +28,11 @@
 sha-1 = "0.10.0"
 twox-hash = "1.6.2"
 same-file = "1.0.6"
-stable_deref_trait = "1.2.0"
-tempfile = "3.3.0"
-crossbeam-channel = "0.5.2"
-micro-timer = "0.4.0"
-log = "0.4.14"
-memmap2 = { version = "0.5.3", features = ["stable_deref_trait"] }
+tempfile = "3.1.0"
+crossbeam-channel = "0.4"
+micro-timer = "0.3.0"
+log = "0.4.8"
+memmap2 = {version = "0.4", features = ["stable_deref_trait"]}
 zstd = "0.5.3"
 format-bytes = "0.3.0"
 
--- a/rust/hg-core/src/dirstate/status.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/dirstate/status.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -136,8 +136,6 @@
     DirstateV2ParseError(DirstateV2ParseError),
 }
 
-pub type StatusResult<T> = Result<T, StatusError>;
-
 impl fmt::Display for StatusError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
--- a/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/dirstate_tree/dirstate_map.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -723,10 +723,11 @@
 
 impl OwningDirstateMap {
     pub fn clear(&mut self) {
-        let map = self.get_map_mut();
-        map.root = Default::default();
-        map.nodes_with_entry_count = 0;
-        map.nodes_with_copy_source_count = 0;
+        self.with_dmap_mut(|map| {
+            map.root = Default::default();
+            map.nodes_with_entry_count = 0;
+            map.nodes_with_copy_source_count = 0;
+        });
     }
 
     pub fn set_entry(
@@ -734,9 +735,10 @@
         filename: &HgPath,
         entry: DirstateEntry,
     ) -> Result<(), DirstateV2ParseError> {
-        let map = self.get_map_mut();
-        map.get_or_insert(&filename)?.data = NodeData::Entry(entry);
-        Ok(())
+        self.with_dmap_mut(|map| {
+            map.get_or_insert(&filename)?.data = NodeData::Entry(entry);
+            Ok(())
+        })
     }
 
     pub fn add_file(
@@ -745,8 +747,9 @@
         entry: DirstateEntry,
     ) -> Result<(), DirstateError> {
         let old_state = self.get(filename)?.map(|e| e.state());
-        let map = self.get_map_mut();
-        Ok(map.add_or_remove_file(filename, old_state, entry)?)
+        self.with_dmap_mut(|map| {
+            Ok(map.add_or_remove_file(filename, old_state, entry)?)
+        })
     }
 
     pub fn remove_file(
@@ -777,9 +780,10 @@
         if size == 0 {
             self.copy_map_remove(filename)?;
         }
-        let map = self.get_map_mut();
-        let entry = DirstateEntry::new_removed(size);
-        Ok(map.add_or_remove_file(filename, old_state, entry)?)
+        self.with_dmap_mut(|map| {
+            let entry = DirstateEntry::new_removed(size);
+            Ok(map.add_or_remove_file(filename, old_state, entry)?)
+        })
     }
 
     pub fn drop_entry_and_copy_source(
@@ -789,7 +793,6 @@
         let was_tracked = self
             .get(filename)?
             .map_or(false, |e| e.state().is_tracked());
-        let map = self.get_map_mut();
         struct Dropped {
             was_tracked: bool,
             had_entry: bool,
@@ -826,10 +829,20 @@
                 )? {
                     dropped = d;
                     if dropped.had_entry {
-                        node.descendants_with_entry_count -= 1;
+                        node.descendants_with_entry_count = node
+                            .descendants_with_entry_count
+                            .checked_sub(1)
+                            .expect(
+                                "descendants_with_entry_count should be >= 0",
+                            );
                     }
                     if dropped.was_tracked {
-                        node.tracked_descendants_count -= 1;
+                        node.tracked_descendants_count = node
+                            .tracked_descendants_count
+                            .checked_sub(1)
+                            .expect(
+                                "tracked_descendants_count should be >= 0",
+                            );
                     }
 
                     // Directory caches must be invalidated when removing a
@@ -843,21 +856,22 @@
                     return Ok(None);
                 }
             } else {
-                let had_entry = node.data.has_entry();
+                let entry = node.data.as_entry();
+                let was_tracked = entry.map_or(false, |entry| entry.tracked());
+                let had_entry = entry.is_some();
                 if had_entry {
                     node.data = NodeData::None
                 }
+                let mut had_copy_source = false;
                 if let Some(source) = &node.copy_source {
                     DirstateMap::count_dropped_path(unreachable_bytes, source);
+                    had_copy_source = true;
                     node.copy_source = None
                 }
                 dropped = Dropped {
-                    was_tracked: node
-                        .data
-                        .as_entry()
-                        .map_or(false, |entry| entry.state().is_tracked()),
+                    was_tracked,
                     had_entry,
-                    had_copy_source: node.copy_source.take().is_some(),
+                    had_copy_source,
                 };
             }
             // After recursion, for both leaf (rest_of_path is None) nodes and
@@ -876,52 +890,62 @@
             Ok(Some((dropped, remove)))
         }
 
-        if let Some((dropped, _removed)) = recur(
-            map.on_disk,
-            &mut map.unreachable_bytes,
-            &mut map.root,
-            filename,
-        )? {
-            if dropped.had_entry {
-                map.nodes_with_entry_count -= 1
+        self.with_dmap_mut(|map| {
+            if let Some((dropped, _removed)) = recur(
+                map.on_disk,
+                &mut map.unreachable_bytes,
+                &mut map.root,
+                filename,
+            )? {
+                if dropped.had_entry {
+                    map.nodes_with_entry_count = map
+                        .nodes_with_entry_count
+                        .checked_sub(1)
+                        .expect("nodes_with_entry_count should be >= 0");
+                }
+                if dropped.had_copy_source {
+                    map.nodes_with_copy_source_count = map
+                        .nodes_with_copy_source_count
+                        .checked_sub(1)
+                        .expect("nodes_with_copy_source_count should be >= 0");
+                }
+            } else {
+                debug_assert!(!was_tracked);
             }
-            if dropped.had_copy_source {
-                map.nodes_with_copy_source_count -= 1
-            }
-        } else {
-            debug_assert!(!was_tracked);
-        }
-        Ok(())
+            Ok(())
+        })
     }
 
     pub fn has_tracked_dir(
         &mut self,
         directory: &HgPath,
     ) -> Result<bool, DirstateError> {
-        let map = self.get_map_mut();
-        if let Some(node) = map.get_node(directory)? {
-            // A node without a `DirstateEntry` was created to hold child
-            // nodes, and is therefore a directory.
-            let state = node.state()?;
-            Ok(state.is_none() && node.tracked_descendants_count() > 0)
-        } else {
-            Ok(false)
-        }
+        self.with_dmap_mut(|map| {
+            if let Some(node) = map.get_node(directory)? {
+                // A node without a `DirstateEntry` was created to hold child
+                // nodes, and is therefore a directory.
+                let state = node.state()?;
+                Ok(state.is_none() && node.tracked_descendants_count() > 0)
+            } else {
+                Ok(false)
+            }
+        })
     }
 
     pub fn has_dir(
         &mut self,
         directory: &HgPath,
     ) -> Result<bool, DirstateError> {
-        let map = self.get_map_mut();
-        if let Some(node) = map.get_node(directory)? {
-            // A node without a `DirstateEntry` was created to hold child
-            // nodes, and is therefore a directory.
-            let state = node.state()?;
-            Ok(state.is_none() && node.descendants_with_entry_count() > 0)
-        } else {
-            Ok(false)
-        }
+        self.with_dmap_mut(|map| {
+            if let Some(node) = map.get_node(directory)? {
+                // A node without a `DirstateEntry` was created to hold child
+                // nodes, and is therefore a directory.
+                let state = node.state()?;
+                Ok(state.is_none() && node.descendants_with_entry_count() > 0)
+            } else {
+                Ok(false)
+            }
+        })
     }
 
     #[timed]
@@ -973,16 +997,29 @@
         on_disk::write(map, can_append)
     }
 
-    pub fn status<'a>(
-        &'a mut self,
-        matcher: &'a (dyn Matcher + Sync),
+    /// `callback` allows the caller to process and do something with the
+    /// results of the status. This is needed to do so efficiently (i.e.
+    /// without cloning the `DirstateStatus` object with its paths) because
+    /// we need to borrow from `Self`.
+    pub fn with_status<R>(
+        &mut self,
+        matcher: &(dyn Matcher + Sync),
         root_dir: PathBuf,
         ignore_files: Vec<PathBuf>,
         options: StatusOptions,
-    ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
-    {
-        let map = self.get_map_mut();
-        super::status::status(map, matcher, root_dir, ignore_files, options)
+        callback: impl for<'r> FnOnce(
+            Result<(DirstateStatus<'r>, Vec<PatternFileWarning>), StatusError>,
+        ) -> R,
+    ) -> R {
+        self.with_dmap_mut(|map| {
+            callback(super::status::status(
+                map,
+                matcher,
+                root_dir,
+                ignore_files,
+                options,
+            ))
+        })
     }
 
     pub fn copy_map_len(&self) -> usize {
@@ -1030,22 +1067,23 @@
         &mut self,
         key: &HgPath,
     ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
-        let map = self.get_map_mut();
-        let count = &mut map.nodes_with_copy_source_count;
-        let unreachable_bytes = &mut map.unreachable_bytes;
-        Ok(DirstateMap::get_node_mut(
-            map.on_disk,
-            unreachable_bytes,
-            &mut map.root,
-            key,
-        )?
-        .and_then(|node| {
-            if let Some(source) = &node.copy_source {
-                *count -= 1;
-                DirstateMap::count_dropped_path(unreachable_bytes, source);
-            }
-            node.copy_source.take().map(Cow::into_owned)
-        }))
+        self.with_dmap_mut(|map| {
+            let count = &mut map.nodes_with_copy_source_count;
+            let unreachable_bytes = &mut map.unreachable_bytes;
+            Ok(DirstateMap::get_node_mut(
+                map.on_disk,
+                unreachable_bytes,
+                &mut map.root,
+                key,
+            )?
+            .and_then(|node| {
+                if let Some(source) = &node.copy_source {
+                    *count -= 1;
+                    DirstateMap::count_dropped_path(unreachable_bytes, source);
+                }
+                node.copy_source.take().map(Cow::into_owned)
+            }))
+        })
     }
 
     pub fn copy_map_insert(
@@ -1053,19 +1091,20 @@
         key: HgPathBuf,
         value: HgPathBuf,
     ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
-        let map = self.get_map_mut();
-        let node = DirstateMap::get_or_insert_node(
-            map.on_disk,
-            &mut map.unreachable_bytes,
-            &mut map.root,
-            &key,
-            WithBasename::to_cow_owned,
-            |_ancestor| {},
-        )?;
-        if node.copy_source.is_none() {
-            map.nodes_with_copy_source_count += 1
-        }
-        Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
+        self.with_dmap_mut(|map| {
+            let node = DirstateMap::get_or_insert_node(
+                map.on_disk,
+                &mut map.unreachable_bytes,
+                &mut map.root,
+                &key,
+                WithBasename::to_cow_owned,
+                |_ancestor| {},
+            )?;
+            if node.copy_source.is_none() {
+                map.nodes_with_copy_source_count += 1
+            }
+            Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
+        })
     }
 
     pub fn len(&self) -> usize {
@@ -1113,7 +1152,7 @@
         >,
         DirstateError,
     > {
-        let map = self.get_map_mut();
+        let map = self.get_map();
         let on_disk = map.on_disk;
         Ok(Box::new(filter_map_results(
             map.iter_nodes(),
--- a/rust/hg-core/src/dirstate_tree/owning.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/dirstate_tree/owning.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -1,105 +1,89 @@
+use crate::{DirstateError, DirstateParents};
+
 use super::dirstate_map::DirstateMap;
-use stable_deref_trait::StableDeref;
 use std::ops::Deref;
 
+use ouroboros::self_referencing;
+
 /// Keep a `DirstateMap<'on_disk>` next to the `on_disk` buffer that it
 /// borrows.
-///
-/// This is similar to [`OwningRef`] which is more limited because it
-/// represents exactly one `&T` reference next to the value it borrows, as
-/// opposed to a struct that may contain an arbitrary number of references in
-/// arbitrarily-nested data structures.
-///
-/// [`OwningRef`]: https://docs.rs/owning_ref/0.4.1/owning_ref/struct.OwningRef.html
+#[self_referencing]
 pub struct OwningDirstateMap {
-    /// Owned handle to a bytes buffer with a stable address.
-    ///
-    /// See <https://docs.rs/owning_ref/0.4.1/owning_ref/trait.StableAddress.html>.
     on_disk: Box<dyn Deref<Target = [u8]> + Send>,
-
-    /// Pointer for `Box<DirstateMap<'on_disk>>`, typed-erased because the
-    /// language cannot represent a lifetime referencing a sibling field.
-    /// This is not quite a self-referencial struct (moving this struct is not
-    /// a problem as it doesn’t change the address of the bytes buffer owned
-    /// by `on_disk`) but touches similar borrow-checker limitations.
-    ptr: *mut (),
+    #[borrows(on_disk)]
+    #[covariant]
+    map: DirstateMap<'this>,
 }
 
 impl OwningDirstateMap {
     pub fn new_empty<OnDisk>(on_disk: OnDisk) -> Self
     where
-        OnDisk: Deref<Target = [u8]> + StableDeref + Send + 'static,
+        OnDisk: Deref<Target = [u8]> + Send + 'static,
     {
         let on_disk = Box::new(on_disk);
-        let bytes: &'_ [u8] = &on_disk;
-        let map = DirstateMap::empty(bytes);
 
-        // Like in `bytes` above, this `'_` lifetime parameter borrows from
-        // the bytes buffer owned by `on_disk`.
-        let ptr: *mut DirstateMap<'_> = Box::into_raw(Box::new(map));
-
-        // Erase the pointed type entirely in order to erase the lifetime.
-        let ptr: *mut () = ptr.cast();
-
-        Self { on_disk, ptr }
+        OwningDirstateMapBuilder {
+            on_disk,
+            map_builder: |bytes| DirstateMap::empty(&bytes),
+        }
+        .build()
     }
 
-    pub fn get_pair_mut<'a>(
-        &'a mut self,
-    ) -> (&'a [u8], &'a mut DirstateMap<'a>) {
-        // SAFETY: We cast the type-erased pointer back to the same type it had
-        // in `new`, except with a different lifetime parameter. This time we
-        // connect the lifetime to that of `self`. This cast is valid because
-        // `self` owns the same `on_disk` whose buffer `DirstateMap`
-        // references. That buffer has a stable memory address because our
-        // `Self::new_empty` counstructor requires `StableDeref`.
-        let ptr: *mut DirstateMap<'a> = self.ptr.cast();
-        // SAFETY: we dereference that pointer, connecting the lifetime of the
-        // new `&mut` to that of `self`. This is valid because the
-        // raw pointer is to a boxed value, and `self` owns that box.
-        (&self.on_disk, unsafe { &mut *ptr })
-    }
+    pub fn new_v1<OnDisk>(
+        on_disk: OnDisk,
+    ) -> Result<(Self, DirstateParents), DirstateError>
+    where
+        OnDisk: Deref<Target = [u8]> + Send + 'static,
+    {
+        let on_disk = Box::new(on_disk);
+        let mut parents = DirstateParents::NULL;
 
-    pub fn get_map_mut<'a>(&'a mut self) -> &'a mut DirstateMap<'a> {
-        self.get_pair_mut().1
+        Ok((
+            OwningDirstateMapTryBuilder {
+                on_disk,
+                map_builder: |bytes| {
+                    DirstateMap::new_v1(&bytes).map(|(dmap, p)| {
+                        parents = p.unwrap_or(DirstateParents::NULL);
+                        dmap
+                    })
+                },
+            }
+            .try_build()?,
+            parents,
+        ))
     }
 
-    pub fn get_map<'a>(&'a self) -> &'a DirstateMap<'a> {
-        // SAFETY: same reasoning as in `get_pair_mut` above.
-        let ptr: *mut DirstateMap<'a> = self.ptr.cast();
-        unsafe { &*ptr }
+    pub fn new_v2<OnDisk>(
+        on_disk: OnDisk,
+        data_size: usize,
+        metadata: &[u8],
+    ) -> Result<Self, DirstateError>
+    where
+        OnDisk: Deref<Target = [u8]> + Send + 'static,
+    {
+        let on_disk = Box::new(on_disk);
+
+        OwningDirstateMapTryBuilder {
+            on_disk,
+            map_builder: |bytes| {
+                DirstateMap::new_v2(&bytes, data_size, metadata)
+            },
+        }
+        .try_build()
     }
 
-    pub fn on_disk<'a>(&'a self) -> &'a [u8] {
-        &self.on_disk
+    pub fn with_dmap_mut<R>(
+        &mut self,
+        f: impl FnOnce(&mut DirstateMap) -> R,
+    ) -> R {
+        self.with_map_mut(f)
+    }
+
+    pub fn get_map(&self) -> &DirstateMap {
+        self.borrow_map()
+    }
+
+    pub fn on_disk(&self) -> &[u8] {
+        self.borrow_on_disk()
     }
 }
-
-impl Drop for OwningDirstateMap {
-    fn drop(&mut self) {
-        // Silence a "field is never read" warning, and demonstrate that this
-        // value is still alive.
-        let _: &Box<dyn Deref<Target = [u8]> + Send> = &self.on_disk;
-        // SAFETY: this cast is the same as in `get_mut`, and is valid for the
-        // same reason. `self.on_disk` still exists at this point, drop glue
-        // will drop it implicitly after this `drop` method returns.
-        let ptr: *mut DirstateMap<'_> = self.ptr.cast();
-        // SAFETY: `Box::from_raw` takes ownership of the box away from `self`.
-        // This is fine because drop glue does nothing for `*mut ()` and we’re
-        // in `drop`, so `get` and `get_mut` cannot be called again.
-        unsafe { drop(Box::from_raw(ptr)) }
-    }
-}
-
-fn _static_assert_is_send<T: Send>() {}
-
-fn _static_assert_fields_are_send() {
-    _static_assert_is_send::<Box<DirstateMap<'_>>>();
-}
-
-// SAFETY: we don’t get this impl implicitly because `*mut (): !Send` because
-// thread-safety of raw pointers is unknown in the general case. However this
-// particular raw pointer represents a `Box<DirstateMap<'on_disk>>` that we
-// own. Since that `Box` is `Send` as shown in above, it is sound to mark
-// this struct as `Send` too.
-unsafe impl Send for OwningDirstateMap {}
--- a/rust/hg-core/src/dirstate_tree/status.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/dirstate_tree/status.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -40,13 +40,14 @@
 /// exists in one of the two trees, depending on information requested by
 /// `options` we may need to traverse the remaining subtree.
 #[timed]
-pub fn status<'tree, 'on_disk: 'tree>(
-    dmap: &'tree mut DirstateMap<'on_disk>,
+pub fn status<'dirstate>(
+    dmap: &'dirstate mut DirstateMap,
     matcher: &(dyn Matcher + Sync),
     root_dir: PathBuf,
     ignore_files: Vec<PathBuf>,
     options: StatusOptions,
-) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
+) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
+{
     // Force the global rayon threadpool to not exceed 16 concurrent threads.
     // This is a stop-gap measure until we figure out why using more than 16
     // threads makes `status` slower for each additional thread.
--- a/rust/hg-core/src/repo.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/repo.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -1,7 +1,6 @@
 use crate::changelog::Changelog;
 use crate::config::{Config, ConfigError, ConfigParseError};
 use crate::dirstate::DirstateParents;
-use crate::dirstate_tree::dirstate_map::DirstateMap;
 use crate::dirstate_tree::on_disk::Docket as DirstateDocket;
 use crate::dirstate_tree::owning::OwningDirstateMap;
 use crate::errors::HgResultExt;
@@ -316,25 +315,19 @@
                 .set(Some(docket.uuid.to_owned()));
             let data_size = docket.data_size();
             let metadata = docket.tree_metadata();
-            let mut map = if let Some(data_mmap) = self
+            if let Some(data_mmap) = self
                 .hg_vfs()
                 .mmap_open(docket.data_filename())
                 .io_not_found_as_none()?
             {
-                OwningDirstateMap::new_empty(data_mmap)
+                OwningDirstateMap::new_v2(data_mmap, data_size, metadata)
             } else {
-                OwningDirstateMap::new_empty(Vec::new())
-            };
-            let (on_disk, placeholder) = map.get_pair_mut();
-            *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
-            Ok(map)
+                OwningDirstateMap::new_v2(Vec::new(), data_size, metadata)
+            }
         } else {
-            let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
-            let (on_disk, placeholder) = map.get_pair_mut();
-            let (inner, parents) = DirstateMap::new_v1(on_disk)?;
-            self.dirstate_parents
-                .set(parents.unwrap_or(DirstateParents::NULL));
-            *placeholder = inner;
+            let (map, parents) =
+                OwningDirstateMap::new_v1(dirstate_file_contents)?;
+            self.dirstate_parents.set(parents);
             Ok(map)
         }
     }
--- a/rust/hg-core/src/utils/hg_path.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-core/src/utils/hg_path.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -144,15 +144,8 @@
 /// On Unix, it's just byte-to-byte conversion. On Windows, it has to be
 /// decoded from MBCS to WTF-8. If WindowsUTF8Plan is implemented, the source
 /// character encoding will be determined on a per-repository basis.
-//
-// FIXME: (adapted from a comment in the stdlib)
-// `HgPath::new()` current implementation relies on `Slice` being
-// layout-compatible with `[u8]`.
-// When attribute privacy is implemented, `Slice` should be annotated as
-// `#[repr(transparent)]`.
-// Anyway, `Slice` representation and layout are considered implementation
-// detail, are not documented and must not be relied upon.
 #[derive(Eq, Ord, PartialEq, PartialOrd, Hash)]
+#[repr(transparent)]
 pub struct HgPath {
     inner: [u8],
 }
--- a/rust/hg-cpython/src/dirstate/dirstate_map.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-cpython/src/dirstate/dirstate_map.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -23,7 +23,6 @@
 };
 use hg::{
     dirstate::StateMapIter,
-    dirstate_tree::dirstate_map::DirstateMap as TreeDirstateMap,
     dirstate_tree::on_disk::DirstateV2ParseError,
     dirstate_tree::owning::OwningDirstateMap,
     revlog::Node,
@@ -53,18 +52,12 @@
         on_disk: PyBytes,
     ) -> PyResult<PyObject> {
         let on_disk = PyBytesDeref::new(py, on_disk);
-        let mut map = OwningDirstateMap::new_empty(on_disk);
-        let (on_disk, map_placeholder) = map.get_pair_mut();
-
-        let (actual_map, parents) = TreeDirstateMap::new_v1(on_disk)
+        let (map, parents) = OwningDirstateMap::new_v1(on_disk)
             .map_err(|e| dirstate_error(py, e))?;
-        *map_placeholder = actual_map;
         let map = Self::create_instance(py, map)?;
-        let parents = parents.map(|p| {
-            let p1 = PyBytes::new(py, p.p1.as_bytes());
-            let p2 = PyBytes::new(py, p.p2.as_bytes());
-            (p1, p2)
-        });
+        let p1 = PyBytes::new(py, parents.p1.as_bytes());
+        let p2 = PyBytes::new(py, parents.p2.as_bytes());
+        let parents = (p1, p2);
         Ok((map, parents).to_py_object(py).into_object())
     }
 
@@ -79,9 +72,7 @@
             PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
         };
         let on_disk = PyBytesDeref::new(py, on_disk);
-        let mut map = OwningDirstateMap::new_empty(on_disk);
-        let (on_disk, map_placeholder) = map.get_pair_mut();
-        *map_placeholder = TreeDirstateMap::new_v2(
+        let map = OwningDirstateMap::new_v2(
             on_disk, data_size, tree_metadata.data(py),
         ).map_err(dirstate_error)?;
         let map = Self::create_instance(py, map)?;
--- a/rust/hg-cpython/src/dirstate/status.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/hg-cpython/src/dirstate/status.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -127,25 +127,29 @@
     // The caller may call `copymap.items()` separately
     let list_copies = false;
 
+    let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
+        let (status_res, warnings) =
+            res.map_err(|e| handle_fallback(py, e))?;
+        build_response(py, status_res, warnings)
+    };
+
     match matcher.get_type(py).name(py).borrow() {
         "alwaysmatcher" => {
             let matcher = AlwaysMatcher;
-            let (status_res, warnings) = dmap
-                .status(
-                    &matcher,
-                    root_dir.to_path_buf(),
-                    ignore_files,
-                    StatusOptions {
-                        check_exec,
-                        list_clean,
-                        list_ignored,
-                        list_unknown,
-                        list_copies,
-                        collect_traversed_dirs,
-                    },
-                )
-                .map_err(|e| handle_fallback(py, e))?;
-            build_response(py, status_res, warnings)
+            dmap.with_status(
+                &matcher,
+                root_dir.to_path_buf(),
+                ignore_files,
+                StatusOptions {
+                    check_exec,
+                    list_clean,
+                    list_ignored,
+                    list_unknown,
+                    list_copies,
+                    collect_traversed_dirs,
+                },
+                after_status,
+            )
         }
         "exactmatcher" => {
             let files = matcher.call_method(
@@ -167,22 +171,20 @@
             let files = files?;
             let matcher = FileMatcher::new(files.as_ref())
                 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
-            let (status_res, warnings) = dmap
-                .status(
-                    &matcher,
-                    root_dir.to_path_buf(),
-                    ignore_files,
-                    StatusOptions {
-                        check_exec,
-                        list_clean,
-                        list_ignored,
-                        list_unknown,
-                        list_copies,
-                        collect_traversed_dirs,
-                    },
-                )
-                .map_err(|e| handle_fallback(py, e))?;
-            build_response(py, status_res, warnings)
+            dmap.with_status(
+                &matcher,
+                root_dir.to_path_buf(),
+                ignore_files,
+                StatusOptions {
+                    check_exec,
+                    list_clean,
+                    list_ignored,
+                    list_unknown,
+                    list_copies,
+                    collect_traversed_dirs,
+                },
+                after_status,
+            )
         }
         "includematcher" => {
             // Get the patterns from Python even though most of them are
@@ -219,23 +221,20 @@
             let matcher = IncludeMatcher::new(ignore_patterns)
                 .map_err(|e| handle_fallback(py, e.into()))?;
 
-            let (status_res, warnings) = dmap
-                .status(
-                    &matcher,
-                    root_dir.to_path_buf(),
-                    ignore_files,
-                    StatusOptions {
-                        check_exec,
-                        list_clean,
-                        list_ignored,
-                        list_unknown,
-                        list_copies,
-                        collect_traversed_dirs,
-                    },
-                )
-                .map_err(|e| handle_fallback(py, e))?;
-
-            build_response(py, status_res, warnings)
+            dmap.with_status(
+                &matcher,
+                root_dir.to_path_buf(),
+                ignore_files,
+                StatusOptions {
+                    check_exec,
+                    list_clean,
+                    list_ignored,
+                    list_unknown,
+                    list_copies,
+                    collect_traversed_dirs,
+                },
+                after_status,
+            )
         }
         e => Err(PyErr::new::<ValueError, _>(
             py,
--- a/rust/rhg/src/commands/status.rs	Wed Mar 30 00:57:08 2022 -0400
+++ b/rust/rhg/src/commands/status.rs	Tue Apr 05 11:09:03 2022 +0200
@@ -25,6 +25,9 @@
 use hg::utils::files::get_bytes_from_path;
 use hg::utils::files::get_path_from_bytes;
 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
+use hg::DirstateStatus;
+use hg::PatternFileWarning;
+use hg::StatusError;
 use hg::StatusOptions;
 use log::info;
 use std::io;
@@ -230,117 +233,132 @@
         list_copies,
         collect_traversed_dirs: false,
     };
-    let (mut ds_status, pattern_warnings) = dmap.status(
-        &AlwaysMatcher,
-        repo.working_directory_path().to_owned(),
-        ignore_files(repo, config),
-        options,
-    )?;
-    for warning in pattern_warnings {
-        match warning {
-            hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
-                .write_stderr(&format_bytes!(
-                    b"{}: ignoring invalid syntax '{}'\n",
-                    get_bytes_from_path(path),
-                    &*syntax
-                ))?,
-            hg::PatternFileWarning::NoSuchFile(path) => {
-                let path = if let Ok(relative) =
-                    path.strip_prefix(repo.working_directory_path())
-                {
-                    relative
-                } else {
-                    &*path
-                };
-                ui.write_stderr(&format_bytes!(
-                    b"skipping unreadable pattern file '{}': \
-                      No such file or directory\n",
-                    get_bytes_from_path(path),
-                ))?
+
+    type StatusResult<'a> =
+        Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
+
+    let after_status = |res: StatusResult| -> Result<_, CommandError> {
+        let (mut ds_status, pattern_warnings) = res?;
+        for warning in pattern_warnings {
+            match warning {
+                hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
+                    .write_stderr(&format_bytes!(
+                        b"{}: ignoring invalid syntax '{}'\n",
+                        get_bytes_from_path(path),
+                        &*syntax
+                    ))?,
+                hg::PatternFileWarning::NoSuchFile(path) => {
+                    let path = if let Ok(relative) =
+                        path.strip_prefix(repo.working_directory_path())
+                    {
+                        relative
+                    } else {
+                        &*path
+                    };
+                    ui.write_stderr(&format_bytes!(
+                        b"skipping unreadable pattern file '{}': \
+                          No such file or directory\n",
+                        get_bytes_from_path(path),
+                    ))?
+                }
             }
         }
-    }
 
-    for (path, error) in ds_status.bad {
-        let error = match error {
-            hg::BadMatch::OsError(code) => {
-                std::io::Error::from_raw_os_error(code).to_string()
-            }
-            hg::BadMatch::BadType(ty) => {
-                format!("unsupported file type (type is {})", ty)
-            }
-        };
-        ui.write_stderr(&format_bytes!(
-            b"{}: {}\n",
-            path.as_bytes(),
-            error.as_bytes()
-        ))?
-    }
-    if !ds_status.unsure.is_empty() {
-        info!(
-            "Files to be rechecked by retrieval from filelog: {:?}",
-            ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
-        );
-    }
-    let mut fixup = Vec::new();
-    if !ds_status.unsure.is_empty()
-        && (display_states.modified || display_states.clean)
-    {
-        let p1 = repo.dirstate_parents()?.p1;
-        let manifest = repo.manifest_for_node(p1).map_err(|e| {
-            CommandError::from((e, &*format!("{:x}", p1.short())))
-        })?;
-        for to_check in ds_status.unsure {
-            if unsure_is_modified(repo, &manifest, &to_check.path)? {
-                if display_states.modified {
-                    ds_status.modified.push(to_check);
+        for (path, error) in ds_status.bad {
+            let error = match error {
+                hg::BadMatch::OsError(code) => {
+                    std::io::Error::from_raw_os_error(code).to_string()
+                }
+                hg::BadMatch::BadType(ty) => {
+                    format!("unsupported file type (type is {})", ty)
                 }
-            } else {
-                if display_states.clean {
-                    ds_status.clean.push(to_check.clone());
+            };
+            ui.write_stderr(&format_bytes!(
+                b"{}: {}\n",
+                path.as_bytes(),
+                error.as_bytes()
+            ))?
+        }
+        if !ds_status.unsure.is_empty() {
+            info!(
+                "Files to be rechecked by retrieval from filelog: {:?}",
+                ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
+            );
+        }
+        let mut fixup = Vec::new();
+        if !ds_status.unsure.is_empty()
+            && (display_states.modified || display_states.clean)
+        {
+            let p1 = repo.dirstate_parents()?.p1;
+            let manifest = repo.manifest_for_node(p1).map_err(|e| {
+                CommandError::from((e, &*format!("{:x}", p1.short())))
+            })?;
+            for to_check in ds_status.unsure {
+                if unsure_is_modified(repo, &manifest, &to_check.path)? {
+                    if display_states.modified {
+                        ds_status.modified.push(to_check);
+                    }
+                } else {
+                    if display_states.clean {
+                        ds_status.clean.push(to_check.clone());
+                    }
+                    fixup.push(to_check.path.into_owned())
                 }
-                fixup.push(to_check.path.into_owned())
             }
         }
-    }
-    let relative_paths = (!ui.plain(None))
-        && config
-            .get_option(b"commands", b"status.relative")?
-            .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
-    let output = DisplayStatusPaths {
-        ui,
-        no_status,
-        relativize: if relative_paths {
-            Some(RelativizePaths::new(repo)?)
-        } else {
-            None
-        },
+        let relative_paths = (!ui.plain(None))
+            && config
+                .get_option(b"commands", b"status.relative")?
+                .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
+        let output = DisplayStatusPaths {
+            ui,
+            no_status,
+            relativize: if relative_paths {
+                Some(RelativizePaths::new(repo)?)
+            } else {
+                None
+            },
+        };
+        if display_states.modified {
+            output.display(b"M ", "status.modified", ds_status.modified)?;
+        }
+        if display_states.added {
+            output.display(b"A ", "status.added", ds_status.added)?;
+        }
+        if display_states.removed {
+            output.display(b"R ", "status.removed", ds_status.removed)?;
+        }
+        if display_states.deleted {
+            output.display(b"! ", "status.deleted", ds_status.deleted)?;
+        }
+        if display_states.unknown {
+            output.display(b"? ", "status.unknown", ds_status.unknown)?;
+        }
+        if display_states.ignored {
+            output.display(b"I ", "status.ignored", ds_status.ignored)?;
+        }
+        if display_states.clean {
+            output.display(b"C ", "status.clean", ds_status.clean)?;
+        }
+
+        let dirstate_write_needed = ds_status.dirty;
+        let filesystem_time_at_status_start =
+            ds_status.filesystem_time_at_status_start;
+
+        Ok((
+            fixup,
+            dirstate_write_needed,
+            filesystem_time_at_status_start,
+        ))
     };
-    if display_states.modified {
-        output.display(b"M ", "status.modified", ds_status.modified)?;
-    }
-    if display_states.added {
-        output.display(b"A ", "status.added", ds_status.added)?;
-    }
-    if display_states.removed {
-        output.display(b"R ", "status.removed", ds_status.removed)?;
-    }
-    if display_states.deleted {
-        output.display(b"! ", "status.deleted", ds_status.deleted)?;
-    }
-    if display_states.unknown {
-        output.display(b"? ", "status.unknown", ds_status.unknown)?;
-    }
-    if display_states.ignored {
-        output.display(b"I ", "status.ignored", ds_status.ignored)?;
-    }
-    if display_states.clean {
-        output.display(b"C ", "status.clean", ds_status.clean)?;
-    }
-
-    let mut dirstate_write_needed = ds_status.dirty;
-    let filesystem_time_at_status_start =
-        ds_status.filesystem_time_at_status_start;
+    let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
+        dmap.with_status(
+            &AlwaysMatcher,
+            repo.working_directory_path().to_owned(),
+            ignore_files(repo, config),
+            options,
+            after_status,
+        )?;
 
     if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
         && !dirstate_write_needed