# HG changeset patch # User Raphaël Gomès # Date 2024-06-19 12:49:35 # Node ID db7dbe6f7bb2b7eca836fcc38fb0c2cd85c96ee7 # Parent 69b804c8e09e7df1d15924b4633fd4d6e0e995e9 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. diff --git a/rust/Cargo.lock b/rust/Cargo.lock --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -180,7 +180,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -435,10 +435,16 @@ dependencies = [ "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 @@ dependencies = [ ] [[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 @@ dependencies = [ "clap", "crossbeam-channel", "derive_more", + "dyn-clone", + "filetime", "flate2", "format-bytes", "hashbrown 0.13.1", @@ -752,6 +772,7 @@ checksum = "c0ff37bd590ca25063e35af745c3 dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.3", ] [[package]] @@ -1123,6 +1144,15 @@ dependencies = [ ] [[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 @@ dependencies = [ "cfg-if", "fastrand", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "remove_dir_all", "winapi", ] @@ -1615,6 +1645,15 @@ dependencies = [ ] [[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 @@ dependencies = [ [[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 @@ checksum = "2b38e32f0abccf9987a4e3079dfb [[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 @@ checksum = "dc35310971f3b2dbbf3f0690a219 [[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 @@ checksum = "a75915e7def60c94dcef72200b9a [[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 @@ checksum = "8f55c233f70c4b27f66c523580f7 [[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 @@ checksum = "53d40abd2583d23e4718fddf1ebe [[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 @@ checksum = "0b7b52767868a23d5bab768e390d [[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 @@ checksum = "ed94fce61571a4006852b7389a06 [[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" diff --git a/rust/hg-core/Cargo.toml b/rust/hg-core/Cargo.toml --- a/rust/hg-core/Cargo.toml +++ b/rust/hg-core/Cargo.toml @@ -41,6 +41,8 @@ format-bytes = "0.3.0" 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. diff --git a/rust/hg-core/src/lock.rs b/rust/hg-core/src/lock.rs --- a/rust/hg-core/src/lock.rs +++ b/rust/hg-core/src/lock.rs @@ -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 @@ pub enum LockError { /// 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( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, f: impl FnOnce() -> R, ) -> Result { @@ -57,7 +57,7 @@ pub fn try_with_lock_no_wait( 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 @@ fn break_lock(hg_vfs: Vfs, lock_filename #[cfg(unix)] fn make_lock( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, data: &str, ) -> Result<(), HgError> { @@ -82,7 +82,7 @@ fn make_lock( } fn read_lock( - hg_vfs: Vfs, + hg_vfs: &VfsImpl, lock_filename: &str, ) -> Result, HgError> { let link_target = @@ -98,7 +98,7 @@ fn read_lock( } } -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) } diff --git a/rust/hg-core/src/logging.rs b/rust/hg-core/src/logging.rs --- a/rust/hg-core/src/logging.rs +++ b/rust/hg-core/src/logging.rs @@ -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 @@ use std::io::Write; /// "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, 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 @@ impl<'a> LogFile<'a> { #[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(); diff --git a/rust/hg-core/src/repo.rs b/rust/hg-core/src/repo.rs --- a/rust/hg-core/src/repo.rs +++ b/rust/hg-core/src/repo.rs @@ -18,7 +18,7 @@ use crate::utils::debug::debug_wait_for_ 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 @@ impl Repo { 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 @@ impl Repo { 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 @@ impl Repo { } } 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 @@ impl Repo { /// 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 @@ impl Repo { &self, f: impl FnOnce() -> R, ) -> Result { - 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. diff --git a/rust/hg-core/src/requirements.rs b/rust/hg-core/src/requirements.rs --- a/rust/hg-core/src/requirements.rs +++ b/rust/hg-core/src/requirements.rs @@ -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, HgError> { @@ -24,11 +24,13 @@ fn parse(bytes: &[u8]) -> Result Result, HgError> { +pub(crate) fn load(hg_vfs: VfsImpl) -> Result, HgError> { parse(&hg_vfs.read("requires")?) } -pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result, HgError> { +pub(crate) fn load_if_exists( + hg_vfs: &VfsImpl, +) -> Result, HgError> { if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? { parse(&bytes) } else { diff --git a/rust/hg-core/src/revlog/changelog.rs b/rust/hg-core/src/revlog/changelog.rs --- a/rust/hg-core/src/revlog/changelog.rs +++ b/rust/hg-core/src/revlog/changelog.rs @@ -13,7 +13,7 @@ use crate::revlog::Revision; 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 @@ pub struct Changelog { impl Changelog { /// Open the `changelog` of a repository given by its root. pub fn open( - store_vfs: &Vfs, + store_vfs: &VfsImpl, options: RevlogOpenOptions, ) -> Result { let revlog = Revlog::open(store_vfs, "00changelog.i", None, options)?; @@ -500,7 +500,7 @@ fn unescape_extra(bytes: &[u8]) -> Vec 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( diff --git a/rust/hg-core/src/revlog/filelog.rs b/rust/hg-core/src/revlog/filelog.rs --- a/rust/hg-core/src/revlog/filelog.rs +++ b/rust/hg-core/src/revlog/filelog.rs @@ -29,7 +29,7 @@ impl Graph for Filelog { impl Filelog { pub fn open_vfs( - store_vfs: &crate::vfs::Vfs<'_>, + store_vfs: &crate::vfs::VfsImpl, file_path: &HgPath, options: RevlogOpenOptions, ) -> Result { diff --git a/rust/hg-core/src/revlog/manifest.rs b/rust/hg-core/src/revlog/manifest.rs --- a/rust/hg-core/src/revlog/manifest.rs +++ b/rust/hg-core/src/revlog/manifest.rs @@ -3,7 +3,7 @@ use crate::revlog::{Node, NodePrefix}; 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 Graph for Manifestlog { impl Manifestlog { /// Open the `manifest` of a repository given by its root. pub fn open( - store_vfs: &Vfs, + store_vfs: &VfsImpl, options: RevlogOpenOptions, ) -> Result { let revlog = Revlog::open(store_vfs, "00manifest.i", None, options)?; diff --git a/rust/hg-core/src/revlog/mod.rs b/rust/hg-core/src/revlog/mod.rs --- a/rust/hg-core/src/revlog/mod.rs +++ b/rust/hg-core/src/revlog/mod.rs @@ -38,7 +38,7 @@ use crate::exit_codes; 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 @@ impl Revlog { /// 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, data_path: Option<&Path>, options: RevlogOpenOptions, @@ -717,7 +718,8 @@ impl Revlog { } 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, data_path: Option<&Path>, options: RevlogOpenOptions, @@ -1298,7 +1300,9 @@ mod tests { #[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 @@ mod tests { #[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 @@ mod tests { #[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 diff --git a/rust/hg-core/src/revlog/nodemap_docket.rs b/rust/hg-core/src/revlog/nodemap_docket.rs --- a/rust/hg-core/src/revlog/nodemap_docket.rs +++ b/rust/hg-core/src/revlog/nodemap_docket.rs @@ -3,7 +3,7 @@ use bytes_cast::{unaligned, BytesCast}; 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 @@ impl NodeMapDocket { /// * 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, HgError> { let docket_path = index_path.with_extension("n"); diff --git a/rust/hg-core/src/vfs.rs b/rust/hg-core/src/vfs.rs --- a/rust/hg-core/src/vfs.rs +++ b/rust/hg-core/src/vfs.rs @@ -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) -> PathBuf { self.base.join(relative_path) } @@ -71,7 +75,12 @@ impl Vfs<'_> { } 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 @@ impl Vfs<'_> { relative_path: impl AsRef, 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 @@ fn fs_metadata( } } +/// 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; + fn open_read(&self, filename: &Path) -> Result; + fn open_check_ambig( + &self, + filename: &Path, + ) -> Result; + fn create(&self, filename: &Path) -> Result; + /// Must truncate the new file if exist + fn create_atomic( + &self, + filename: &Path, + check_ambig: bool, + ) -> Result; + fn file_size(&self, file: &File) -> Result; + 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 { + todo!() + } + fn open_read(&self, filename: &Path) -> Result { + let path = self.base.join(filename); + std::fs::File::open(&path).when_reading_file(&path) + } + fn open_check_ambig( + &self, + _filename: &Path, + ) -> Result { + todo!() + } + fn create(&self, _filename: &Path) -> Result { + todo!() + } + fn create_atomic( + &self, + _filename: &Path, + _check_ambig: bool, + ) -> Result { + todo!() + } + fn file_size(&self, file: &File) -> Result { + 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) -> Result { Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir())) } diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs +++ b/rust/rhg/src/commands/status.rs @@ -393,8 +393,8 @@ pub fn run(invocation: &crate::CliInvoca // + 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 @@ enum UnsureOutcome { /// 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 @@ fn unsure_is_modified( return Ok(UnsureOutcome::Modified); } let filelog = hg::filelog::Filelog::open_vfs( - &store_vfs, + store_vfs, hg_path, revlog_open_options, )?;