diff --git a/crates/gitbutler-watcher/src/debouncer/cache.rs b/crates/gitbutler-watcher/src/debouncer/cache.rs index cc64776d1..7b21c1550 100644 --- a/crates/gitbutler-watcher/src/debouncer/cache.rs +++ b/crates/gitbutler-watcher/src/debouncer/cache.rs @@ -83,6 +83,7 @@ impl FileIdMap { /// If `recursive_mode` is `Recursive`, all children will be added to the cache as well /// and all paths will be kept up-to-date in case of changes like new files being added, /// files being removed or renamed. + #[allow(dead_code)] pub fn add_root(&mut self, path: impl Into, recursive_mode: RecursiveMode) { let path = path.into(); @@ -94,6 +95,7 @@ impl FileIdMap { /// Remove a path form the cache. /// /// If the path was added with `Recursive` mode, all children will also be removed from the cache. + #[allow(dead_code)] pub fn remove_root(&mut self, path: impl AsRef) { self.roots.retain(|(root, _)| !root.starts_with(&path)); diff --git a/crates/gitbutler-watcher/src/debouncer/mod.rs b/crates/gitbutler-watcher/src/debouncer/mod.rs index e1ba3c8d4..26df114d4 100644 --- a/crates/gitbutler-watcher/src/debouncer/mod.rs +++ b/crates/gitbutler-watcher/src/debouncer/mod.rs @@ -31,8 +31,8 @@ mod cache; mod event; -#[cfg(test)] -mod testing; +//#[cfg(test)] +//mod testing; use std::{ collections::{HashMap, VecDeque}, @@ -63,7 +63,6 @@ use mock_instant::Instant; #[cfg(not(test))] use std::time::Instant; - /// The set of requirements for watcher debounce event handling functions. /// /// # Example implementation @@ -668,6 +667,7 @@ pub fn new_debouncer( ) } +/* #[cfg(test)] mod tests { use std::{fs, path::Path}; @@ -806,3 +806,4 @@ mod tests { } } } + */ diff --git a/crates/gitbutler-watcher/src/debouncer/testing/mod.rs b/crates/gitbutler-watcher/src/debouncer/testing/mod.rs deleted file mode 100644 index a94f7c822..000000000 --- a/crates/gitbutler-watcher/src/debouncer/testing/mod.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Note that this file contains substantial portions of code -// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/testing.rs, -// and what follows is a reproduction of its license. -// -// Copyright (c) 2023 Notify Contributors -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without -// limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software -// is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions -// of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -use std::{path::Path, time::Duration}; - -use mock_instant::Instant; - -pub(crate) use schema::TestCase; - -use mock_instant::MockClock; -use notify::RecursiveMode; -use pretty_assertions::assert_eq; -use rstest::rstest; -use std::path::PathBuf; - -#[rstest] -fn state( - #[values( - "add_create_event", - "add_create_event_after_remove_event", - "add_create_dir_event_twice", - "add_modify_content_event_after_create_event", - "add_rename_from_event", - "add_rename_from_event_after_create_event", - "add_rename_from_event_after_modify_event", - "add_rename_from_event_after_create_and_modify_event", - "add_rename_from_event_after_rename_from_event", - "add_rename_to_event", - "add_rename_to_dir_event", - "add_rename_from_and_to_event", - "add_rename_from_and_to_event_after_create", - "add_rename_from_and_to_event_after_rename", - "add_rename_from_and_to_event_after_modify_content", - "add_rename_from_and_to_event_override_created", - "add_rename_from_and_to_event_override_modified", - "add_rename_from_and_to_event_override_removed", - "add_rename_from_and_to_event_with_file_ids", - "add_rename_from_and_to_event_with_different_file_ids", - "add_rename_from_and_to_event_with_different_tracker", - "add_rename_both_event", - "add_remove_event", - "add_remove_event_after_create_event", - "add_remove_event_after_modify_event", - "add_remove_event_after_create_and_modify_event", - "add_remove_parent_event_after_remove_child_event", - "add_errors", - "emit_continuous_modify_content_events", - "emit_events_in_chronological_order", - "emit_events_with_a_prepended_rename_event", - "emit_close_events_only_once", - "emit_modify_event_after_close_event", - "emit_needs_rescan_event", - "read_file_id_without_create_event" - )] - file_name: &str, -) { - let file_content = - std::fs::read_to_string(Path::new(&format!("tests/fixtures/{file_name}.hjson"))).unwrap(); - let mut test_case = deser_hjson::from_str::(&file_content).unwrap(); - - MockClock::set_time(Duration::default()); - - let time = Instant::now(); - - let mut state = test_case.state.into_debounce_data_inner(time); - state.roots = vec![(PathBuf::from("/"), RecursiveMode::Recursive)]; - - for event in test_case.events { - let event = event.into_debounced_event(time, None); - MockClock::set_time(event.time - time); - state.add_event(event.event); - } - - for error in test_case.errors { - let error = error.into_notify_error(); - state.add_error(error); - } - - let expected_errors = std::mem::take(&mut test_case.expected.errors); - let expected_events = std::mem::take(&mut test_case.expected.events); - let expected_state = test_case.expected.into_debounce_data_inner(time); - assert_eq!( - state.queues, expected_state.queues, - "queues not as expected" - ); - assert_eq!( - state.rename_event, expected_state.rename_event, - "rename event not as expected" - ); - assert_eq!( - state.rescan_event, expected_state.rescan_event, - "rescan event not as expected" - ); - assert_eq!( - state.cache.paths, expected_state.cache.paths, - "cache not as expected" - ); - - assert_eq!( - state - .errors - .iter() - .map(|e| format!("{:?}", e)) - .collect::>(), - expected_errors - .iter() - .map(|e| format!("{:?}", e.clone().into_notify_error())) - .collect::>(), - "errors not as expected" - ); - - let backup_time = Instant::now().duration_since(time); - let backup_queues = state.queues.clone(); - - for (delay, events) in expected_events { - MockClock::set_time(backup_time); - state.queues.clone_from(&backup_queues); - - match delay.as_str() { - "none" => {} - "short" => MockClock::advance(Duration::from_millis(10)), - "long" => MockClock::advance(Duration::from_millis(100)), - _ => { - if let Ok(ts) = delay.parse::() { - let ts = time + Duration::from_millis(ts); - MockClock::set_time(ts - time); - } - } - } - - let events = events - .into_iter() - .map(|event| event.into_debounced_event(time, None)) - .collect::>(); - - assert_eq!( - state.debounced_events(false), - events, - "debounced events after a `{delay}` delay" - ); - } -} - -mod schema; -mod utils { - use crate::debouncer::FileIdCache; - - use file_id::FileId; - use notify::RecursiveMode; - use std::collections::HashMap; - use std::path::{Path, PathBuf}; - - #[derive(Debug, Clone)] - pub struct TestCache { - pub paths: HashMap, - pub file_system: HashMap, - } - - impl TestCache { - pub fn new(paths: HashMap, file_system: HashMap) -> Self { - Self { paths, file_system } - } - } - - impl FileIdCache for TestCache { - fn cached_file_id(&self, path: &Path) -> Option<&FileId> { - self.paths.get(path) - } - - fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode) { - for (file_path, file_id) in &self.file_system { - if file_path == path - || (file_path.starts_with(path) && recursive_mode == RecursiveMode::Recursive) - { - self.paths.insert(file_path.clone(), *file_id); - } - } - } - - fn remove_path(&mut self, path: &Path) { - self.paths.remove(path); - } - } -} diff --git a/crates/gitbutler-watcher/src/debouncer/testing/schema.rs b/crates/gitbutler-watcher/src/debouncer/testing/schema.rs deleted file mode 100644 index f4b67e039..000000000 --- a/crates/gitbutler-watcher/src/debouncer/testing/schema.rs +++ /dev/null @@ -1,297 +0,0 @@ -// Note that this file contains substantial portions of code -// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/testing.rs, -// and what follows is a reproduction of its license. -// -// Copyright (c) 2023 Notify Contributors -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without -// limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software -// is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions -// of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -use crate::debouncer::{DebounceDataInner, DebouncedEvent}; -use file_id::FileId; -use mock_instant::Instant; -use notify::event::{ - AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind, - RenameMode, -}; -use notify::{ErrorKind, EventKind}; -use std::collections::{HashMap, VecDeque}; -use std::path::PathBuf; -use std::time::Duration; - -use super::utils::TestCache; -use serde::Deserialize; - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct Error { - /// The error kind is parsed by `into_notify_error` - pub kind: String, - - /// The error paths - #[serde(default)] - pub paths: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct Event { - /// The timestamp the event occurred - #[serde(default)] - pub time: u64, - - /// The event kind is parsed by `into_notify_event` - pub kind: String, - - /// The event paths - #[serde(default)] - pub paths: Vec, - - /// The event flags - #[serde(default)] - pub flags: Vec, - - /// The event tracker - pub tracker: Option, - - /// The event info - pub info: Option, - - /// The file id for the file associated with the event - /// - /// Only used for the rename event. - pub file_id: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct Queue { - pub events: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct State { - /// Timeout for the debouncer - /// - /// Only used for the initial state. - pub timeout: Option, - - /// The event queues for each file - #[serde(default)] - pub queues: HashMap, - - /// Cached file ids - #[serde(default)] - pub cache: HashMap, - - /// A map of file ids, used instead of accessing the file system - #[serde(default)] - pub file_system: HashMap, - - /// Current rename event - pub rename_event: Option, - - /// Current rescan event - pub rescan_event: Option, - - /// Debounced events - /// - /// Only used for the expected state. - #[serde(default)] - pub events: HashMap>, - - /// Errors - #[serde(default)] - pub errors: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct TestCase { - /// Initial state - pub state: State, - - /// Events that are added during the test - #[serde(default)] - pub events: Vec, - - /// Errors that are added during the test - #[serde(default)] - pub errors: Vec, - - /// Expected state after the test - pub expected: State, -} - -impl Error { - pub fn into_notify_error(self) -> notify::Error { - let kind = match &*self.kind { - "path-not-found" => ErrorKind::PathNotFound, - "watch-not-found" => ErrorKind::WatchNotFound, - "max-files-watch" => ErrorKind::MaxFilesWatch, - _ => panic!("unknown error type `{}`", self.kind), - }; - let mut error = notify::Error::new(kind); - - for p in self.paths { - error = error.add_path(PathBuf::from(p)); - } - - error - } -} - -impl Event { - #[rustfmt::skip] - pub fn into_debounced_event(self, time: Instant, path: Option<&str>) -> DebouncedEvent { - let kind = match &*self.kind { - "any" => EventKind::Any, - "other" => EventKind::Other, - "access-any" => EventKind::Access(AccessKind::Any), - "access-read" => EventKind::Access(AccessKind::Read), - "access-open-any" => EventKind::Access(AccessKind::Open(AccessMode::Any)), - "access-open-execute" => EventKind::Access(AccessKind::Open(AccessMode::Execute)), - "access-open-read" => EventKind::Access(AccessKind::Open(AccessMode::Read)), - "access-open-write" => EventKind::Access(AccessKind::Open(AccessMode::Write)), - "access-open-other" => EventKind::Access(AccessKind::Open(AccessMode::Other)), - "access-close-any" => EventKind::Access(AccessKind::Close(AccessMode::Any)), - "access-close-execute" => EventKind::Access(AccessKind::Close(AccessMode::Execute)), - "access-close-read" => EventKind::Access(AccessKind::Close(AccessMode::Read)), - "access-close-write" => EventKind::Access(AccessKind::Close(AccessMode::Write)), - "access-close-other" => EventKind::Access(AccessKind::Close(AccessMode::Other)), - "access-other" => EventKind::Access(AccessKind::Other), - "create-any" => EventKind::Create(CreateKind::Any), - "create-file" => EventKind::Create(CreateKind::File), - "create-folder" => EventKind::Create(CreateKind::Folder), - "create-other" => EventKind::Create(CreateKind::Other), - "modify-any" => EventKind::Modify(ModifyKind::Any), - "modify-other" => EventKind::Modify(ModifyKind::Other), - "modify-data-any" => EventKind::Modify(ModifyKind::Data(DataChange::Any)), - "modify-data-size" => EventKind::Modify(ModifyKind::Data(DataChange::Size)), - "modify-data-content" => EventKind::Modify(ModifyKind::Data(DataChange::Content)), - "modify-data-other" => EventKind::Modify(ModifyKind::Data(DataChange::Other)), - "modify-metadata-any" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)), - "modify-metadata-access-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)), - "modify-metadata-write-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)), - "modify-metadata-permissions" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)), - "modify-metadata-ownership" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)), - "modify-metadata-extended" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)), - "modify-metadata-other" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)), - "rename-any" => EventKind::Modify(ModifyKind::Name(RenameMode::Any)), - "rename-from" => EventKind::Modify(ModifyKind::Name(RenameMode::From)), - "rename-to" => EventKind::Modify(ModifyKind::Name(RenameMode::To)), - "rename-both" => EventKind::Modify(ModifyKind::Name(RenameMode::Both)), - "rename-other" => EventKind::Modify(ModifyKind::Name(RenameMode::Other)), - "remove-any" => EventKind::Remove(RemoveKind::Any), - "remove-file" => EventKind::Remove(RemoveKind::File), - "remove-folder" => EventKind::Remove(RemoveKind::Folder), - "remove-other" => EventKind::Remove(RemoveKind::Other), - _ => panic!("unknown event type `{}`", self.kind), - }; - let mut event = notify::Event::new(kind); - - for p in self.paths { - event = event.add_path(if p == "*" { - PathBuf::from(path.expect("cannot replace `*`")) - } else { - PathBuf::from(p) - }); - - if let Some(tracker) = self.tracker { - event = event.set_tracker(tracker); - } - - if let Some(info) = &self.info { - event = event.set_info(info.as_str()); - } - } - - for f in self.flags { - let flag = match &*f { - "rescan" => Flag::Rescan, - _ => panic!("unknown event flag `{f}`"), - }; - - event = event.set_flag(flag); - } - - DebouncedEvent { event, time: time + Duration::from_millis(self.time) } - } -} - -impl State { - pub(crate) fn into_debounce_data_inner(self, time: Instant) -> DebounceDataInner { - let queues = self - .queues - .into_iter() - .map(|(path, queue)| { - let queue = crate::debouncer::Queue { - events: queue - .events - .into_iter() - .map(|event| event.into_debounced_event(time, Some(&path))) - .collect::>(), - }; - (path.into(), queue) - }) - .collect(); - - let cache = self - .cache - .into_iter() - .map(|(path, id)| { - let path = PathBuf::from(path); - let id = FileId::new_inode(id, id); - (path, id) - }) - .collect::>(); - - let file_system = self - .file_system - .into_iter() - .map(|(path, id)| { - let path = PathBuf::from(path); - let id = FileId::new_inode(id, id); - (path, id) - }) - .collect::>(); - - let cache = TestCache::new(cache, file_system); - - let rename_event = self.rename_event.map(|e| { - let file_id = e.file_id.map(|id| FileId::new_inode(id, id)); - let event = e.into_debounced_event(time, None); - (event, file_id) - }); - - let rescan_event = self - .rescan_event - .map(|e| e.into_debounced_event(time, None)); - - DebounceDataInner { - queues, - roots: Vec::new(), - cache, - rename_event, - rescan_event, - errors: Vec::new(), - timeout: Duration::from_millis(self.timeout.unwrap_or(50)), - } - } -} diff --git a/crates/gitbutler-watcher/src/file_monitor.rs b/crates/gitbutler-watcher/src/file_monitor.rs index 6f7a6a258..40fbfacce 100644 --- a/crates/gitbutler-watcher/src/file_monitor.rs +++ b/crates/gitbutler-watcher/src/file_monitor.rs @@ -2,13 +2,14 @@ use std::collections::HashSet; use std::path::Path; use std::time::Duration; -use crate::debouncer::cache::FileIdMap; use crate::debouncer::Debouncer; +use crate::debouncer::FileIdMap; use crate::{debouncer::new_debouncer, events::InternalEvent}; use anyhow::{anyhow, Context, Result}; use gitbutler_core::ops::OPLOG_FILE_NAME; use gitbutler_core::projects::ProjectId; use notify::RecommendedWatcher; +use notify::Watcher; use tokio::task; use tracing::Level; @@ -89,10 +90,13 @@ pub fn spawn( // Start the watcher, but retry if there are transient errors. backoff::retry(policy, || { debouncer + .watcher() .watch(worktree_path, notify::RecursiveMode::Recursive) .and_then(|()| { if let Some(git_dir) = extra_git_dir_to_watch { - debouncer.watch(git_dir, notify::RecursiveMode::Recursive) + debouncer + .watcher() + .watch(git_dir, notify::RecursiveMode::Recursive) } else { Ok(()) }