rust: add Vfs trait
authorRaphaël Gomès <rgomes@octobus.net>
Wed, 19 Jun 2024 14:49:35 +0200
changeset 51868 db7dbe6f7bb2
parent 51867 69b804c8e09e
child 51870 9d4ad05bc91c
rust: add Vfs trait This will allow for the use of multiple vfs like in the Python implementation, as well as hiding the details of the upcoming Python vfs wrapper to hg-core.
rust/Cargo.lock
rust/hg-core/Cargo.toml
rust/hg-core/src/lock.rs
rust/hg-core/src/logging.rs
rust/hg-core/src/repo.rs
rust/hg-core/src/requirements.rs
rust/hg-core/src/revlog/changelog.rs
rust/hg-core/src/revlog/filelog.rs
rust/hg-core/src/revlog/manifest.rs
rust/hg-core/src/revlog/mod.rs
rust/hg-core/src/revlog/nodemap_docket.rs
rust/hg-core/src/vfs.rs
rust/rhg/src/commands/status.rs
--- a/rust/Cargo.lock	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/Cargo.lock	Wed Jun 19 14:49:35 2024 +0200
@@ -180,7 +180,7 @@
  "js-sys",
  "num-traits",
  "wasm-bindgen",
- "windows-targets 0.52.0",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -435,10 +435,16 @@
  "libc",
  "option-ext",
  "redox_users",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
+name = "dyn-clone"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+
+[[package]]
 name = "either"
 version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -467,6 +473,18 @@
 ]
 
 [[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "flate2"
 version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -584,6 +602,8 @@
  "clap",
  "crossbeam-channel",
  "derive_more",
+ "dyn-clone",
+ "filetime",
  "flate2",
  "format-bytes",
  "hashbrown 0.13.1",
@@ -752,6 +772,7 @@
 dependencies = [
  "bitflags 2.6.0",
  "libc",
+ "redox_syscall 0.5.3",
 ]
 
 [[package]]
@@ -1123,6 +1144,15 @@
 ]
 
 [[package]]
+name = "redox_syscall"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
 name = "redox_users"
 version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1348,7 +1378,7 @@
  "cfg-if",
  "fastrand",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.2.16",
  "remove_dir_all",
  "winapi",
 ]
@@ -1615,6 +1645,15 @@
 ]
 
 [[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
 name = "windows-targets"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1631,17 +1670,18 @@
 
 [[package]]
 name = "windows-targets"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.0",
- "windows_aarch64_msvc 0.52.0",
- "windows_i686_gnu 0.52.0",
- "windows_i686_msvc 0.52.0",
- "windows_x86_64_gnu 0.52.0",
- "windows_x86_64_gnullvm 0.52.0",
- "windows_x86_64_msvc 0.52.0",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -1652,9 +1692,9 @@
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -1664,9 +1704,9 @@
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -1676,9 +1716,15 @@
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -1688,9 +1734,9 @@
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -1700,9 +1746,9 @@
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -1712,9 +1758,9 @@
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -1724,9 +1770,9 @@
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "wyz"
--- a/rust/hg-core/Cargo.toml	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/Cargo.toml	Wed Jun 19 14:49:35 2024 +0200
@@ -41,6 +41,8 @@
 once_cell = "1.16.0"
 bitvec = "1.0.1"
 chrono = "0.4.34"
+dyn-clone = "1.0.16"
+filetime = "0.2.23"
 
 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
 # we have a clearer view of which backend is the fastest.
--- a/rust/hg-core/src/lock.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/lock.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -2,7 +2,7 @@
 
 use crate::errors::HgError;
 use crate::errors::HgResultExt;
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 use std::io;
 use std::io::ErrorKind;
 
@@ -21,7 +21,7 @@
 /// The return value of `f` is dropped in that case. If all is successful, the
 /// return value of `f` is forwarded.
 pub fn try_with_lock_no_wait<R>(
-    hg_vfs: Vfs,
+    hg_vfs: &VfsImpl,
     lock_filename: &str,
     f: impl FnOnce() -> R,
 ) -> Result<R, LockError> {
@@ -57,7 +57,7 @@
     Err(LockError::AlreadyHeld)
 }
 
-fn break_lock(hg_vfs: Vfs, lock_filename: &str) -> Result<(), LockError> {
+fn break_lock(hg_vfs: &VfsImpl, lock_filename: &str) -> Result<(), LockError> {
     try_with_lock_no_wait(hg_vfs, &format!("{}.break", lock_filename), || {
         // Check again in case some other process broke and
         // acquired the lock in the meantime
@@ -71,7 +71,7 @@
 
 #[cfg(unix)]
 fn make_lock(
-    hg_vfs: Vfs,
+    hg_vfs: &VfsImpl,
     lock_filename: &str,
     data: &str,
 ) -> Result<(), HgError> {
@@ -82,7 +82,7 @@
 }
 
 fn read_lock(
-    hg_vfs: Vfs,
+    hg_vfs: &VfsImpl,
     lock_filename: &str,
 ) -> Result<Option<String>, HgError> {
     let link_target =
@@ -98,7 +98,7 @@
     }
 }
 
-fn unlock(hg_vfs: Vfs, lock_filename: &str) -> Result<(), HgError> {
+fn unlock(hg_vfs: &VfsImpl, lock_filename: &str) -> Result<(), HgError> {
     hg_vfs.remove_file(lock_filename)
 }
 
--- a/rust/hg-core/src/logging.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/logging.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -1,5 +1,5 @@
 use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 use std::io::Write;
 
 /// An utility to append to a log file with the given name, and optionally
@@ -9,14 +9,14 @@
 /// "example.log.1" to "example.log.2" etc up to the given maximum number of
 /// files.
 pub struct LogFile<'a> {
-    vfs: Vfs<'a>,
+    vfs: VfsImpl,
     name: &'a str,
     max_size: Option<u64>,
     max_files: u32,
 }
 
 impl<'a> LogFile<'a> {
-    pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self {
+    pub fn new(vfs: VfsImpl, name: &'a str) -> Self {
         Self {
             vfs,
             name,
@@ -87,8 +87,12 @@
 #[test]
 fn test_rotation() {
     let temp = tempfile::tempdir().unwrap();
-    let vfs = Vfs { base: temp.path() };
-    let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2);
+    let vfs = VfsImpl {
+        base: temp.path().to_owned(),
+    };
+    let logger = LogFile::new(vfs.clone(), "log")
+        .max_size(Some(3))
+        .max_files(2);
     logger.write(b"one\n").unwrap();
     logger.write(b"two\n").unwrap();
     logger.write(b"3\n").unwrap();
--- a/rust/hg-core/src/repo.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/repo.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -18,7 +18,7 @@
 use crate::utils::files::get_path_from_bytes;
 use crate::utils::hg_path::HgPath;
 use crate::utils::SliceExt;
-use crate::vfs::{is_dir, is_file, Vfs};
+use crate::vfs::{is_dir, is_file, VfsImpl};
 use crate::{
     requirements, NodePrefix, RevlogDataConfig, RevlogDeltaConfig,
     RevlogFeatureConfig, RevlogType, RevlogVersionOptions, UncheckedRevision,
@@ -121,8 +121,10 @@
         let mut repo_config_files =
             vec![dot_hg.join("hgrc"), dot_hg.join("hgrc-not-shared")];
 
-        let hg_vfs = Vfs { base: &dot_hg };
-        let mut reqs = requirements::load_if_exists(hg_vfs)?;
+        let hg_vfs = VfsImpl {
+            base: dot_hg.to_owned(),
+        };
+        let mut reqs = requirements::load_if_exists(&hg_vfs)?;
         let relative =
             reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
         let shared =
@@ -163,9 +165,10 @@
 
             store_path = shared_path.join("store");
 
-            let source_is_share_safe =
-                requirements::load(Vfs { base: &shared_path })?
-                    .contains(requirements::SHARESAFE_REQUIREMENT);
+            let source_is_share_safe = requirements::load(VfsImpl {
+                base: shared_path.to_owned(),
+            })?
+            .contains(requirements::SHARESAFE_REQUIREMENT);
 
             if share_safe != source_is_share_safe {
                 return Err(HgError::unsupported("share-safe mismatch").into());
@@ -176,7 +179,9 @@
             }
         }
         if share_safe {
-            reqs.extend(requirements::load(Vfs { base: &store_path })?);
+            reqs.extend(requirements::load(VfsImpl {
+                base: store_path.to_owned(),
+            })?);
         }
 
         let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
@@ -216,19 +221,23 @@
 
     /// For accessing repository files (in `.hg`), except for the store
     /// (`.hg/store`).
-    pub fn hg_vfs(&self) -> Vfs<'_> {
-        Vfs { base: &self.dot_hg }
+    pub fn hg_vfs(&self) -> VfsImpl {
+        VfsImpl {
+            base: self.dot_hg.to_owned(),
+        }
     }
 
     /// For accessing repository store files (in `.hg/store`)
-    pub fn store_vfs(&self) -> Vfs<'_> {
-        Vfs { base: &self.store }
+    pub fn store_vfs(&self) -> VfsImpl {
+        VfsImpl {
+            base: self.store.to_owned(),
+        }
     }
 
     /// For accessing the working copy
-    pub fn working_directory_vfs(&self) -> Vfs<'_> {
-        Vfs {
-            base: &self.working_directory,
+    pub fn working_directory_vfs(&self) -> VfsImpl {
+        VfsImpl {
+            base: self.working_directory.to_owned(),
         }
     }
 
@@ -236,7 +245,7 @@
         &self,
         f: impl FnOnce() -> R,
     ) -> Result<R, LockError> {
-        try_with_lock_no_wait(self.hg_vfs(), "wlock", f)
+        try_with_lock_no_wait(&self.hg_vfs(), "wlock", f)
     }
 
     /// Whether this repo should use dirstate-v2.
--- a/rust/hg-core/src/requirements.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/requirements.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -1,7 +1,7 @@
 use crate::errors::{HgError, HgResultExt};
 use crate::repo::Repo;
 use crate::utils::join_display;
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 use std::collections::HashSet;
 
 fn parse(bytes: &[u8]) -> Result<HashSet<String>, HgError> {
@@ -24,11 +24,13 @@
         .collect()
 }
 
-pub(crate) fn load(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
+pub(crate) fn load(hg_vfs: VfsImpl) -> Result<HashSet<String>, HgError> {
     parse(&hg_vfs.read("requires")?)
 }
 
-pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
+pub(crate) fn load_if_exists(
+    hg_vfs: &VfsImpl,
+) -> Result<HashSet<String>, HgError> {
     if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? {
         parse(&bytes)
     } else {
--- a/rust/hg-core/src/revlog/changelog.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/revlog/changelog.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -13,7 +13,7 @@
 use crate::revlog::{Node, NodePrefix};
 use crate::revlog::{Revlog, RevlogEntry, RevlogError};
 use crate::utils::hg_path::HgPath;
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 use crate::{Graph, GraphError, RevlogOpenOptions, UncheckedRevision};
 
 /// A specialized `Revlog` to work with changelog data format.
@@ -25,7 +25,7 @@
 impl Changelog {
     /// Open the `changelog` of a repository given by its root.
     pub fn open(
-        store_vfs: &Vfs,
+        store_vfs: &VfsImpl,
         options: RevlogOpenOptions,
     ) -> Result<Self, HgError> {
         let revlog = Revlog::open(store_vfs, "00changelog.i", None, options)?;
@@ -500,7 +500,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::vfs::Vfs;
+    use crate::vfs::VfsImpl;
     use crate::{
         RevlogDataConfig, RevlogDeltaConfig, RevlogFeatureConfig,
         NULL_REVISION,
@@ -563,7 +563,9 @@
     fn test_data_from_rev_null() -> Result<(), RevlogError> {
         // an empty revlog will be enough for this case
         let temp = tempfile::tempdir().unwrap();
-        let vfs = Vfs { base: temp.path() };
+        let vfs = VfsImpl {
+            base: temp.path().to_owned(),
+        };
         std::fs::write(temp.path().join("foo.i"), b"").unwrap();
         std::fs::write(temp.path().join("foo.d"), b"").unwrap();
         let revlog = Revlog::open(
--- a/rust/hg-core/src/revlog/filelog.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/revlog/filelog.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -29,7 +29,7 @@
 
 impl Filelog {
     pub fn open_vfs(
-        store_vfs: &crate::vfs::Vfs<'_>,
+        store_vfs: &crate::vfs::VfsImpl,
         file_path: &HgPath,
         options: RevlogOpenOptions,
     ) -> Result<Self, HgError> {
--- a/rust/hg-core/src/revlog/manifest.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/revlog/manifest.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -3,7 +3,7 @@
 use crate::revlog::{Revlog, RevlogError};
 use crate::utils::hg_path::HgPath;
 use crate::utils::SliceExt;
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 use crate::{
     Graph, GraphError, Revision, RevlogOpenOptions, UncheckedRevision,
 };
@@ -23,7 +23,7 @@
 impl Manifestlog {
     /// Open the `manifest` of a repository given by its root.
     pub fn open(
-        store_vfs: &Vfs,
+        store_vfs: &VfsImpl,
         options: RevlogOpenOptions,
     ) -> Result<Self, HgError> {
         let revlog = Revlog::open(store_vfs, "00manifest.i", None, options)?;
--- a/rust/hg-core/src/revlog/mod.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/revlog/mod.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -38,7 +38,7 @@
 use crate::requirements::{
     GENERALDELTA_REQUIREMENT, NARROW_REQUIREMENT, SPARSEREVLOG_REQUIREMENT,
 };
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 
 /// As noted in revlog.c, revision numbers are actually encoded in
 /// 4 bytes, and are liberally converted to ints, whence the i32
@@ -708,7 +708,8 @@
     /// It will also open the associated data file if index and data are not
     /// interleaved.
     pub fn open(
-        store_vfs: &Vfs,
+        // Todo use the `Vfs` trait here once we create a function for mmap
+        store_vfs: &VfsImpl,
         index_path: impl AsRef<Path>,
         data_path: Option<&Path>,
         options: RevlogOpenOptions,
@@ -717,7 +718,8 @@
     }
 
     fn open_gen(
-        store_vfs: &Vfs,
+        // Todo use the `Vfs` trait here once we create a function for mmap
+        store_vfs: &VfsImpl,
         index_path: impl AsRef<Path>,
         data_path: Option<&Path>,
         options: RevlogOpenOptions,
@@ -1298,7 +1300,9 @@
     #[test]
     fn test_empty() {
         let temp = tempfile::tempdir().unwrap();
-        let vfs = Vfs { base: temp.path() };
+        let vfs = VfsImpl {
+            base: temp.path().to_owned(),
+        };
         std::fs::write(temp.path().join("foo.i"), b"").unwrap();
         std::fs::write(temp.path().join("foo.d"), b"").unwrap();
         let revlog =
@@ -1320,7 +1324,9 @@
     #[test]
     fn test_inline() {
         let temp = tempfile::tempdir().unwrap();
-        let vfs = Vfs { base: temp.path() };
+        let vfs = VfsImpl {
+            base: temp.path().to_owned(),
+        };
         let node0 = Node::from_hex("2ed2a3912a0b24502043eae84ee4b279c18b90dd")
             .unwrap();
         let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12")
@@ -1387,7 +1393,9 @@
     #[test]
     fn test_nodemap() {
         let temp = tempfile::tempdir().unwrap();
-        let vfs = Vfs { base: temp.path() };
+        let vfs = VfsImpl {
+            base: temp.path().to_owned(),
+        };
 
         // building a revlog with a forced Node starting with zeros
         // This is a corruption, but it does not preclude using the nodemap
--- a/rust/hg-core/src/revlog/nodemap_docket.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/revlog/nodemap_docket.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -3,7 +3,7 @@
 use memmap2::Mmap;
 use std::path::{Path, PathBuf};
 
-use crate::vfs::Vfs;
+use crate::vfs::VfsImpl;
 
 const ONDISK_VERSION: u8 = 1;
 
@@ -33,7 +33,7 @@
     /// * The docket file points to a missing (likely deleted) data file (this
     ///   can happen in a rare race condition).
     pub fn read_from_file(
-        store_vfs: &Vfs,
+        store_vfs: &VfsImpl,
         index_path: &Path,
     ) -> Result<Option<(Self, Mmap)>, HgError> {
         let docket_path = index_path.with_extension("n");
--- a/rust/hg-core/src/vfs.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/hg-core/src/vfs.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -1,17 +1,21 @@
 use crate::errors::{HgError, IoErrorContext, IoResultExt};
+use crate::exit_codes;
+use dyn_clone::DynClone;
 use memmap2::{Mmap, MmapOptions};
+use std::fs::File;
 use std::io::{ErrorKind, Write};
+use std::os::unix::fs::MetadataExt;
 use std::path::{Path, PathBuf};
 
 /// Filesystem access abstraction for the contents of a given "base" diretory
-#[derive(Clone, Copy)]
-pub struct Vfs<'a> {
-    pub(crate) base: &'a Path,
+#[derive(Clone)]
+pub struct VfsImpl {
+    pub(crate) base: PathBuf,
 }
 
 struct FileNotFound(std::io::Error, PathBuf);
 
-impl Vfs<'_> {
+impl VfsImpl {
     pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
         self.base.join(relative_path)
     }
@@ -71,7 +75,12 @@
             }
             Ok(file) => file,
         };
-        // TODO: what are the safety requirements here?
+        // Safety is "enforced" by locks and assuming other processes are
+        // well-behaved. If any misbehaving or malicious process does touch
+        // the index, it could lead to corruption. This is inherent
+        // to file-based `mmap`, though some platforms have some ways of
+        // mitigating.
+        // TODO linux: set the immutable flag with `chattr(1)`?
         let mmap = unsafe { MmapOptions::new().map(&file) }
             .when_reading_file(&path)?;
         Ok(Ok(mmap))
@@ -134,8 +143,8 @@
         relative_path: impl AsRef<Path>,
         contents: &[u8],
     ) -> Result<(), HgError> {
-        let mut tmp = tempfile::NamedTempFile::new_in(self.base)
-            .when_writing_file(self.base)?;
+        let mut tmp = tempfile::NamedTempFile::new_in(&self.base)
+            .when_writing_file(&self.base)?;
         tmp.write_all(contents)
             .and_then(|()| tmp.flush())
             .when_writing_file(tmp.path())?;
@@ -165,6 +174,174 @@
     }
 }
 
+/// Writable file object that atomically updates a file
+///
+/// All writes will go to a temporary copy of the original file. Call
+/// [`Self::close`] when you are done writing, and [`Self`] will rename
+/// the temporary copy to the original name, making the changes
+/// visible. If the object is destroyed without being closed, all your
+/// writes are discarded.
+pub struct AtomicFile {
+    /// The temporary file to write to
+    fp: std::fs::File,
+    /// Path of the temp file
+    temp_path: PathBuf,
+    /// Used when stat'ing the file, is useful only if the target file is
+    /// guarded by any lock (e.g. repo.lock or repo.wlock).
+    check_ambig: bool,
+    /// Path of the target file
+    target_name: PathBuf,
+    /// Whether the file is open or not
+    is_open: bool,
+}
+
+impl AtomicFile {
+    pub fn new(
+        fp: std::fs::File,
+        check_ambig: bool,
+        temp_name: PathBuf,
+        target_name: PathBuf,
+    ) -> Self {
+        Self {
+            fp,
+            check_ambig,
+            temp_path: temp_name,
+            target_name,
+            is_open: true,
+        }
+    }
+
+    /// Write `buf` to the temporary file
+    pub fn write_all(&mut self, buf: &[u8]) -> Result<(), std::io::Error> {
+        self.fp.write_all(buf)
+    }
+
+    fn target(&self) -> PathBuf {
+        self.temp_path
+            .parent()
+            .expect("should not be at the filesystem root")
+            .join(&self.target_name)
+    }
+
+    /// Close the temporary file and rename to the target
+    pub fn close(mut self) -> Result<(), std::io::Error> {
+        self.fp.flush()?;
+        let target = self.target();
+        if self.check_ambig {
+            if let Ok(stat) = std::fs::metadata(&target) {
+                std::fs::rename(&self.temp_path, &target)?;
+                let new_stat = std::fs::metadata(&target)?;
+                let ctime = new_stat.ctime();
+                let is_ambiguous = ctime == stat.ctime();
+                if is_ambiguous {
+                    let advanced =
+                        filetime::FileTime::from_unix_time(ctime + 1, 0);
+                    filetime::set_file_times(target, advanced, advanced)?;
+                }
+            } else {
+                std::fs::rename(&self.temp_path, target)?;
+            }
+        } else {
+            std::fs::rename(&self.temp_path, target).unwrap();
+        }
+        self.is_open = false;
+        Ok(())
+    }
+}
+
+impl Drop for AtomicFile {
+    fn drop(&mut self) {
+        if self.is_open {
+            std::fs::remove_file(self.target()).ok();
+        }
+    }
+}
+
+/// Abstracts over the VFS to allow for different implementations of the
+/// filesystem layer (like passing one from Python).
+pub trait Vfs: Sync + Send + DynClone {
+    fn open(&self, filename: &Path) -> Result<std::fs::File, HgError>;
+    fn open_read(&self, filename: &Path) -> Result<std::fs::File, HgError>;
+    fn open_check_ambig(
+        &self,
+        filename: &Path,
+    ) -> Result<std::fs::File, HgError>;
+    fn create(&self, filename: &Path) -> Result<std::fs::File, HgError>;
+    /// Must truncate the new file if exist
+    fn create_atomic(
+        &self,
+        filename: &Path,
+        check_ambig: bool,
+    ) -> Result<AtomicFile, HgError>;
+    fn file_size(&self, file: &File) -> Result<u64, HgError>;
+    fn exists(&self, filename: &Path) -> bool;
+    fn unlink(&self, filename: &Path) -> Result<(), HgError>;
+    fn rename(
+        &self,
+        from: &Path,
+        to: &Path,
+        check_ambig: bool,
+    ) -> Result<(), HgError>;
+    fn copy(&self, from: &Path, to: &Path) -> Result<(), HgError>;
+}
+
+/// These methods will need to be implemented once `rhg` (and other) non-Python
+/// users of `hg-core` start doing more on their own, like writing to files.
+impl Vfs for VfsImpl {
+    fn open(&self, _filename: &Path) -> Result<std::fs::File, HgError> {
+        todo!()
+    }
+    fn open_read(&self, filename: &Path) -> Result<std::fs::File, HgError> {
+        let path = self.base.join(filename);
+        std::fs::File::open(&path).when_reading_file(&path)
+    }
+    fn open_check_ambig(
+        &self,
+        _filename: &Path,
+    ) -> Result<std::fs::File, HgError> {
+        todo!()
+    }
+    fn create(&self, _filename: &Path) -> Result<std::fs::File, HgError> {
+        todo!()
+    }
+    fn create_atomic(
+        &self,
+        _filename: &Path,
+        _check_ambig: bool,
+    ) -> Result<AtomicFile, HgError> {
+        todo!()
+    }
+    fn file_size(&self, file: &File) -> Result<u64, HgError> {
+        Ok(file
+            .metadata()
+            .map_err(|e| {
+                HgError::abort(
+                    format!("Could not get file metadata: {}", e),
+                    exit_codes::ABORT,
+                    None,
+                )
+            })?
+            .size())
+    }
+    fn exists(&self, _filename: &Path) -> bool {
+        todo!()
+    }
+    fn unlink(&self, _filename: &Path) -> Result<(), HgError> {
+        todo!()
+    }
+    fn rename(
+        &self,
+        _from: &Path,
+        _to: &Path,
+        _check_ambig: bool,
+    ) -> Result<(), HgError> {
+        todo!()
+    }
+    fn copy(&self, _from: &Path, _to: &Path) -> Result<(), HgError> {
+        todo!()
+    }
+}
+
 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
     Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
 }
--- a/rust/rhg/src/commands/status.rs	Wed Jun 19 12:49:26 2024 +0200
+++ b/rust/rhg/src/commands/status.rs	Wed Jun 19 14:49:35 2024 +0200
@@ -393,8 +393,8 @@
                     // + map_err + collect, so let's just inline some of the
                     // logic.
                     match unsure_is_modified(
-                        working_directory_vfs,
-                        store_vfs,
+                        &working_directory_vfs,
+                        &store_vfs,
                         check_exec,
                         &manifest,
                         &to_check.path,
@@ -748,8 +748,8 @@
 /// This meant to be used for those that the dirstate cannot resolve, due
 /// to time resolution limits.
 fn unsure_is_modified(
-    working_directory_vfs: hg::vfs::Vfs,
-    store_vfs: hg::vfs::Vfs,
+    working_directory_vfs: &hg::vfs::VfsImpl,
+    store_vfs: &hg::vfs::VfsImpl,
     check_exec: bool,
     manifest: &Manifest,
     hg_path: &HgPath,
@@ -786,7 +786,7 @@
         return Ok(UnsureOutcome::Modified);
     }
     let filelog = hg::filelog::Filelog::open_vfs(
-        &store_vfs,
+        store_vfs,
         hg_path,
         revlog_open_options,
     )?;