diff --git a/Cargo.lock b/Cargo.lock index 241e949813..b3af9cbd35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,11 +168,24 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +dependencies = [ + "serde", +] [[package]] name = "cast" @@ -538,6 +551,102 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures 0.1.31", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -565,7 +674,7 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -669,6 +778,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.10.3" @@ -759,7 +877,7 @@ dependencies = [ "backoff", "blake2", "byteorder", - "bytes", + "bytes 1.1.0", "chrono", "config", "git2", @@ -778,8 +896,10 @@ dependencies = [ "tempfile", "test-case", "thiserror", + "tokio", "uuid", "version_check", + "watchman_client", "whoami", "zstd", ] @@ -842,6 +962,16 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -884,6 +1014,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + [[package]] name = "nom" version = "7.1.1" @@ -992,6 +1134,29 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -1047,6 +1212,18 @@ dependencies = [ "sha-1", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.25" @@ -1156,7 +1333,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d644aefa452a3188ce8fd92e71944ad082feca2016eb41d4b9c55ceba9e5f38" dependencies = [ - "bytes", + "bytes 1.1.0", "once_cell", "protobuf-support", "thiserror", @@ -1389,6 +1566,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b929ea725591083cbca8b8ea178ed6efc918eccd40b784e199ce88967104199" +dependencies = [ + "anyhow", + "byteorder", + "bytes 0.4.12", + "serde", + "thiserror", +] + [[package]] name = "serde_cbor" version = "0.11.2" @@ -1445,18 +1635,49 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "similar" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + [[package]] name = "smawk" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1588,7 +1809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -1617,6 +1838,53 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95eec79ea28c00a365f539f1961e9278fbcaf81c0ff6aaf0e93c181352446948" +dependencies = [ + "bytes 1.1.0", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-io", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + [[package]] name = "toml" version = "0.5.9" @@ -1733,6 +2001,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.80" @@ -1787,6 +2061,24 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +[[package]] +name = "watchman_client" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e725bf7c0cf7cd6b413adc8f43e454fcf7caa5cfdf609188dde60eb95bafc5fe" +dependencies = [ + "anyhow", + "bytes 1.1.0", + "futures 0.3.21", + "maplit", + "serde", + "serde_bser", + "thiserror", + "tokio", + "tokio-util", + "winapi", +] + [[package]] name = "web-sys" version = "0.3.57" @@ -1849,6 +2141,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8600ea22e8..012934c9bc 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -36,9 +36,11 @@ regex = "1.5.6" serde_json = "1.0.81" tempfile = "3.3.0" thiserror = "1.0.31" +tokio = "1.18.2" uuid = { version = "1.1.1", features = ["v4"] } whoami = "1.2.1" zstd = "0.11.2" +watchman_client = "0.7.2" [dev-dependencies] assert_matches = "1.5.0" diff --git a/lib/src/fsmonitor.rs b/lib/src/fsmonitor.rs new file mode 100644 index 0000000000..a5b3534671 --- /dev/null +++ b/lib/src/fsmonitor.rs @@ -0,0 +1,114 @@ +//! Interfaces with a filesystem monitor tool (currently Watchman) to +//! efficiently query for filesystem updates, without having to crawl the entire +//! working copy. This is particularly useful for large working copies, or for +//! working copies for which it's expensive to materialize files, such those +//! backed by a network or virtualized filesystem. + +#![warn(missing_docs)] + +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use itertools::Itertools; +use thiserror::Error; +use watchman_client::prelude::{NameOnly, QueryRequestCommon, QueryResult}; + +/// Represents an instance in time from the perspective of the filesystem +/// monitor. +/// +/// This can be used to perform incremental queries. A given query will return +/// the associated clock. By passing the same clock into a future query, this +/// informs the filesystem monitor that we only wish to get changed files since +/// the previous point in time. +#[derive(Clone, Debug)] +pub struct FsmonitorClock(watchman_client::pdu::Clock); + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum FsmonitorError { + #[error("Could not connect to Watchman: {0}")] + WatchmanConnectError(watchman_client::Error), + + #[error("Could not canonicalize working copy root path: {0}")] + CanonicalizeRootError(std::io::Error), + + #[error("Watchman failed to resolve the working copy root path: {0}")] + ResolveRootError(watchman_client::Error), + + #[error("Failed to query Watchman: {0}")] + WatchmanQueryError(watchman_client::Error), +} + +/// Handle to the underlying filesystem monitor (currently Watchman). +#[derive(Clone)] +pub struct Fsmonitor { + client: Arc, + resolved_root: watchman_client::ResolvedRoot, +} + +impl Fsmonitor { + /// Initialize the filesystem monitor. If it's not already running, this + /// will start it and have it crawl the working copy to build up its + /// in-memory representation of the filesystem, which may take some time. + pub async fn init(working_copy_path: &Path) -> Result { + println!("Querying filesystem monitor (Watchman)..."); + let connector = watchman_client::Connector::new(); + let client = connector + .connect() + .await + .map_err(FsmonitorError::WatchmanConnectError)?; + let working_copy_root = watchman_client::CanonicalPath::canonicalize(working_copy_path) + .map_err(FsmonitorError::CanonicalizeRootError)?; + let resolved_root = client + .resolve_root(working_copy_root) + .await + .map_err(FsmonitorError::ResolveRootError)?; + Ok(Self { + client: Arc::new(client), + resolved_root, + }) + } + + /// Query for changed files since the previous point in time. + pub async fn query_changed_files( + &self, + _previous_clock: Option, + ) -> Result<(FsmonitorClock, Option>), FsmonitorError> { + let QueryResult { + version: _, + is_fresh_instance, + files, + clock, + state_enter: _, + state_leave: _, + state_metadata: _, + saved_state_info: _, + debug: _, + }: QueryResult = self + .client + .query( + &self.resolved_root, + QueryRequestCommon { + ..Default::default() + }, + ) + .await + .map_err(FsmonitorError::WatchmanQueryError)?; + + let clock = FsmonitorClock(clock); + if is_fresh_instance { + // The Watchman documentation states that if it was a fresh + // instance, we need to delete any tree entries that didn't appear + // in the returned list of changed files. For now, the caller will + // handle this by manually crawling the working copy again. + Ok((clock, None)) + } else { + let paths: Vec = files + .unwrap_or_default() + .into_iter() + .map(|file_info| file_info.name.into_inner()) + .collect_vec(); + Ok((clock, Some(paths))) + } + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8a2a39c822..bea343cb26 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -30,6 +30,7 @@ pub mod dag_walk; pub mod diff; pub mod file_util; pub mod files; +pub mod fsmonitor; pub mod git; pub mod git_backend; pub mod gitignore; diff --git a/lib/src/settings.rs b/lib/src/settings.rs index cd2d0f83bc..1cf9d729b6 100644 --- a/lib/src/settings.rs +++ b/lib/src/settings.rs @@ -69,6 +69,12 @@ impl UserSettings { .unwrap_or_else(|_| Self::user_email_placeholder().to_string()) } + pub fn use_fsmonitor(&self) -> bool { + self.config + .get_bool("core.use-fsmonitor") + .unwrap_or_default() + } + pub fn user_email_placeholder() -> &'static str { "(no email configured)" } diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 95c049f96d..d429252b39 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -35,6 +35,7 @@ use crate::backend::{ BackendError, ConflictId, FileId, MillisSinceEpoch, SymlinkId, TreeId, TreeValue, }; use crate::conflicts::{materialize_conflict, update_conflict_from_content}; +use crate::fsmonitor::{Fsmonitor, FsmonitorClock, FsmonitorError}; use crate::gitignore::GitIgnoreFile; use crate::lock::FileLock; use crate::matchers::{DifferenceMatcher, Matcher, PrefixMatcher}; @@ -207,6 +208,10 @@ pub struct CheckoutStats { #[derive(Debug, Error)] pub enum SnapshotError { + #[error("Failed to open file {path}: {err:?}")] + FileOpenError { path: PathBuf, err: std::io::Error }, + #[error("Failed to query the filesystem monitor: {0}")] + FsmonitorError(#[from] FsmonitorError), #[error("{message}: {err}")] IoError { message: String, @@ -391,18 +396,92 @@ impl TreeState { Ok(self.store.write_symlink(path, str_target)?) } + #[tokio::main] + async fn query_fsmonitor( + &self, + ) -> Result<(FsmonitorClock, Option>), FsmonitorError> { + let working_copy_path = self.working_copy_path.to_owned(); + tokio::spawn(async move { + let fsmonitor = Fsmonitor::init(&working_copy_path).await?; + let previous_clock = None; + fsmonitor.query_changed_files(previous_clock).await + }) + .await + .unwrap() + } + /// Look for changes to the working copy. If there are any changes, create /// a new tree from it and return it, and also update the dirstate on disk. - pub fn snapshot(&mut self, base_ignores: Arc) -> Result { + pub fn snapshot( + &mut self, + base_ignores: Arc, + should_use_fsmonitor: bool, + ) -> Result { + let (_fsmonitor_clock, changed_files) = if should_use_fsmonitor { + match self.query_fsmonitor() { + Ok((fsmonitor_clock, changed_files)) => (Some(fsmonitor_clock), changed_files), + Err(err) => { + eprintln!("Failed to query fsmonitor: {}", err); + (None, None) + } + } + } else { + (None, None) + }; + let sparse_matcher = self.sparse_matcher(); - let mut work = vec![( - RepoPath::root(), - self.working_copy_path.clone(), - base_ignores, - )]; let mut tree_builder = self.store.tree_builder(self.tree_id.clone()); let mut deleted_files: HashSet<_> = self.file_states.keys().cloned().collect(); - while let Some((dir, disk_dir, git_ignore)) = work.pop() { + + struct WorkItem { + dir: RepoPath, + disk_dir: PathBuf, + git_ignore: Arc, + should_recurse: bool, + } + let repo_root = RepoPath::root(); + let repo_root_pathbuf = repo_root.to_fs_path(&self.working_copy_path); + let mut work: Vec = match changed_files { + Some(changed_files) => changed_files + .into_iter() + .map(|path| { + let repo_path_components = path + .iter() + .map(|component| component.to_str().expect("Non-UTF-8 path component")) + .map(RepoPathComponent::from) + .collect(); + let dir = RepoPath::from_components(repo_path_components); + WorkItem { + dir, + disk_dir: repo_root_pathbuf.join(path), + + // FIXME: it's possible that we missed a `.gitignore` + // which belongs to a parent directory of the given + // file. + git_ignore: base_ignores.clone(), + + should_recurse: false, + } + }) + .collect(), + + None => { + vec![WorkItem { + dir: repo_root, + disk_dir: self.working_copy_path.clone(), + git_ignore: base_ignores, + should_recurse: true, + }] + } + }; + + while let Some(WorkItem { + dir, + disk_dir, + git_ignore, + should_recurse, + }) = work.pop() + { if sparse_matcher.visit(&dir).is_nothing() { continue; } @@ -429,7 +508,15 @@ impl TreeState { { continue; } - work.push((sub_path, entry.path(), git_ignore.clone())); + + if should_recurse { + work.push(WorkItem { + dir: sub_path, + disk_dir: entry.path(), + git_ignore: git_ignore.clone(), + should_recurse: true, + }); + } } else { deleted_files.remove(&sub_path); if sparse_matcher.matches(&sub_path) { @@ -1085,12 +1172,16 @@ impl LockedWorkingCopy<'_> { // The base_ignores are passed in here rather than being set on the TreeState // because the TreeState may be long-lived if the library is used in a // long-lived process. - pub fn snapshot(&mut self, base_ignores: Arc) -> Result { + pub fn snapshot( + &mut self, + base_ignores: Arc, + should_use_fsmonitor: bool, + ) -> Result { self.wc .tree_state() .as_mut() .unwrap() - .snapshot(base_ignores) + .snapshot(base_ignores, should_use_fsmonitor) } pub fn check_out(&mut self, new_tree: &Tree) -> Result { diff --git a/lib/tests/test_working_copy.rs b/lib/tests/test_working_copy.rs index 4fbb72f9be..878c9f104e 100644 --- a/lib/tests/test_working_copy.rs +++ b/lib/tests/test_working_copy.rs @@ -42,7 +42,7 @@ fn test_root(use_git: bool) { let wc = test_workspace.workspace.working_copy_mut(); assert_eq!(wc.sparse_patterns(), vec![RepoPath::root()]); let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); let checkout_id = repo.view().get_checkout(&WorkspaceId::default()).unwrap(); let checkout_commit = repo.store().get_commit(checkout_id).unwrap(); @@ -206,7 +206,7 @@ fn test_checkout_file_transitions(use_git: bool) { // Check that the working copy is clean. let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); assert_eq!(new_tree_id, right_tree_id); @@ -308,7 +308,7 @@ fn test_reset() { assert!(ignored_path.to_fs_path(&workspace_root).is_file()); assert!(!wc.file_states().contains_key(&ignored_path)); let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); assert_eq!(new_tree_id, *tree_without_file.id()); locked_wc.discard(); @@ -321,7 +321,7 @@ fn test_reset() { assert!(ignored_path.to_fs_path(&workspace_root).is_file()); assert!(!wc.file_states().contains_key(&ignored_path)); let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); assert_eq!(new_tree_id, *tree_without_file.id()); locked_wc.discard(); @@ -333,7 +333,7 @@ fn test_reset() { assert!(ignored_path.to_fs_path(&workspace_root).is_file()); assert!(wc.file_states().contains_key(&ignored_path)); let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); assert_eq!(new_tree_id, *tree_with_file.id()); locked_wc.discard(); } @@ -409,7 +409,7 @@ fn test_commit_racy_timestamps(use_git: bool) { .unwrap(); } let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); assert_ne!(new_tree_id, previous_tree_id); previous_tree_id = new_tree_id; @@ -443,7 +443,7 @@ fn test_gitignores(use_git: bool) { let wc = test_workspace.workspace.working_copy_mut(); let mut locked_wc = wc.start_mutation(); - let new_tree_id1 = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id1 = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.finish(repo.op_id().clone()); let tree1 = repo .store() @@ -473,7 +473,7 @@ fn test_gitignores(use_git: bool) { testutils::write_working_copy_file(&workspace_root, &subdir_ignored_path, "2"); let mut locked_wc = wc.start_mutation(); - let new_tree_id2 = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id2 = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); let tree2 = repo .store() @@ -533,7 +533,7 @@ fn test_gitignores_checkout_overwrites_ignored(use_git: bool) { // Check that the file is in the tree created by committing the working copy let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); let new_tree = repo .store() @@ -579,7 +579,7 @@ fn test_gitignores_ignored_directory_already_tracked(use_git: bool) { // Check that the file is still in the tree created by committing the working // copy (that it didn't get removed because the directory is ignored) let mut locked_wc = wc.start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); let new_tree = repo .store() @@ -610,7 +610,7 @@ fn test_dotgit_ignored(use_git: bool) { "contents", ); let mut locked_wc = test_workspace.workspace.working_copy_mut().start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); assert_eq!(new_tree_id, *repo.store().empty_tree_id()); locked_wc.discard(); std::fs::remove_dir_all(&dotgit_path).unwrap(); @@ -622,7 +622,7 @@ fn test_dotgit_ignored(use_git: bool) { "contents", ); let mut locked_wc = test_workspace.workspace.working_copy_mut().start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); assert_eq!(new_tree_id, *repo.store().empty_tree_id()); locked_wc.discard(); } diff --git a/lib/tests/test_working_copy_concurrent.rs b/lib/tests/test_working_copy_concurrent.rs index c6e8857fc8..f8abfcc962 100644 --- a/lib/tests/test_working_copy_concurrent.rs +++ b/lib/tests/test_working_copy_concurrent.rs @@ -131,7 +131,7 @@ fn test_checkout_parallel(use_git: bool) { // write_tree() should take the same lock as check_out(), write_tree() // should never produce a different tree. let mut locked_wc = workspace.working_copy_mut().start_mutation(); - let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let new_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.discard(); assert!(tree_ids.contains(&new_tree_id)); }); diff --git a/lib/tests/test_working_copy_sparse.rs b/lib/tests/test_working_copy_sparse.rs index defa81f195..fd6b45b62a 100644 --- a/lib/tests/test_working_copy_sparse.rs +++ b/lib/tests/test_working_copy_sparse.rs @@ -168,7 +168,7 @@ fn test_sparse_commit() { // Create a tree from the working copy. Only dir1/file1 should be updated in the // tree. let mut locked_wc = wc.start_mutation(); - let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.finish(repo.op_id().clone()); let modified_tree = repo .store() @@ -192,7 +192,7 @@ fn test_sparse_commit() { // Create a tree from the working copy. Only dir1/file1 and dir2/file1 should be // updated in the tree. let mut locked_wc = wc.start_mutation(); - let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.finish(repo.op_id().clone()); let modified_tree = repo .store() @@ -233,7 +233,7 @@ fn test_sparse_commit_gitignore() { // Create a tree from the working copy. Only dir1/file2 should be updated in the // tree because dir1/file1 is ignored. let mut locked_wc = wc.start_mutation(); - let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty()).unwrap(); + let modified_tree_id = locked_wc.snapshot(GitIgnoreFile::empty(), false).unwrap(); locked_wc.finish(repo.op_id().clone()); let modified_tree = repo .store() diff --git a/src/commands.rs b/src/commands.rs index 244daa3d42..8ab031d1e3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -673,7 +673,9 @@ impl WorkspaceCommandHelper { ))); } } - let new_tree_id = locked_wc.snapshot(base_ignores)?; + + let should_use_fsmonitor = ui.settings().use_fsmonitor(); + let new_tree_id = locked_wc.snapshot(base_ignores, should_use_fsmonitor)?; if new_tree_id != *checkout_commit.tree_id() { let mut tx = self.repo.start_transaction("commit working copy"); let mut_repo = tx.mut_repo(); @@ -2133,7 +2135,8 @@ fn cmd_untrack( locked_working_copy.reset(&new_tree)?; // Commit the working copy again so we can inform the user if paths couldn't be // untracked because they're not ignored. - let wc_tree_id = locked_working_copy.snapshot(base_ignores)?; + let should_use_fsmonitor = ui.settings().use_fsmonitor(); + let wc_tree_id = locked_working_copy.snapshot(base_ignores, should_use_fsmonitor)?; if wc_tree_id != new_tree_id { let wc_tree = store.get_tree(&RepoPath::root(), &wc_tree_id)?; let added_back = wc_tree.entries_matching(matcher.as_ref()).collect_vec(); diff --git a/src/diff_edit.rs b/src/diff_edit.rs index ade0905000..a4661b0f7b 100644 --- a/src/diff_edit.rs +++ b/src/diff_edit.rs @@ -171,7 +171,8 @@ pub fn edit_diff( std::fs::remove_file(instructions_path).ok(); } - Ok(right_tree_state.snapshot(base_ignores)?) + let should_use_fsmonitor = ui.settings().use_fsmonitor(); + Ok(right_tree_state.snapshot(base_ignores, should_use_fsmonitor)?) } /// Merge/diff tool loaded from the settings.