diff --git a/crates/gitbutler-tauri/src/watcher.rs b/crates/gitbutler-tauri/src/watcher.rs index a728ef2de..0d09c8ee5 100644 --- a/crates/gitbutler-tauri/src/watcher.rs +++ b/crates/gitbutler-tauri/src/watcher.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use futures::executor::block_on; use gitbutler_core::projects::{self, Project, ProjectId}; -use gitbutler_core::{assets, deltas, sessions, users, virtual_branches}; +use gitbutler_core::{assets, virtual_branches}; use tauri::{AppHandle, Manager}; use tracing::instrument; @@ -113,25 +113,14 @@ pub struct Watchers { } fn handler_from_app(app: &AppHandle) -> anyhow::Result { - let app_data_dir = app - .path_resolver() - .app_data_dir() - .context("failed to get app data dir")?; - let users = app.state::().inner().clone(); let projects = app.state::().inner().clone(); let vbranches = app.state::().inner().clone(); let assets_proxy = app.state::().inner().clone(); - let sessions_db = app.state::().inner().clone(); - let deltas_db = app.state::().inner().clone(); Ok(gitbutler_watcher::Handler::new( - app_data_dir.clone(), - users, projects, vbranches, assets_proxy, - sessions_db, - deltas_db, { let app = app.clone(); move |change| ChangeForFrontend::from(change).send(&app) diff --git a/crates/gitbutler-watcher/src/handler/calculate_deltas.rs b/crates/gitbutler-watcher/src/handler/calculate_deltas.rs deleted file mode 100644 index cc9e38cd3..000000000 --- a/crates/gitbutler-watcher/src/handler/calculate_deltas.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::Change; -use anyhow::{Context, Result}; -use gitbutler_core::{ - deltas, gb_repository, project_repository, projects::ProjectId, reader, sessions, -}; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; -use tracing::instrument; - -impl super::Handler { - #[instrument(skip(self, paths, project_id))] - pub fn calculate_deltas(&self, paths: Vec, project_id: ProjectId) -> Result<()> { - let make_processor = || -> Result<_> { - let project = self - .projects - .get(&project_id) - .context("failed to get project")?; - let project_repository = project_repository::Repository::open(&project) - .with_context(|| "failed to open project repository for project")?; - let user = self.users.get_user().context("failed to get user")?; - let gb_repository = gb_repository::Repository::open( - &self.local_data_dir, - &project_repository, - user.as_ref(), - ) - .context("failed to open gb repository")?; - - // If current session's branch is not the same as the project's head, flush it first. - if let Some(session) = gb_repository - .get_current_session() - .context("failed to get current session")? - { - let project_head = project_repository - .get_head() - .context("failed to get head")?; - if session.meta.branch != project_head.name().map(|n| n.to_string()) { - gb_repository - .flush_session(&project_repository, &session, user.as_ref()) - .context(format!("failed to flush session {}", session.id))?; - } - } - let current_session = gb_repository - .get_or_create_current_session() - .context("failed to get or create current session")?; - let session = current_session.clone(); - - let process = move |path: PathBuf| -> Result { - let _span = tracing::span!(tracing::Level::TRACE, "processing", ?path).entered(); - let current_session_reader = - sessions::Reader::open(&gb_repository, ¤t_session) - .context("failed to get session reader")?; - let deltas_reader = deltas::Reader::new(¤t_session_reader); - let writer = - deltas::Writer::new(&gb_repository).context("failed to open deltas writer")?; - let current_wd_file_content = match Self::file_content(&project_repository, &path) { - Ok(content) => Some(content), - Err(reader::Error::NotFound) => None, - Err(err) => Err(err).context("failed to get file content")?, - }; - let latest_file_content = match current_session_reader.file(&path) { - Ok(content) => Some(content), - Err(reader::Error::NotFound) => None, - Err(err) => Err(err).context("failed to get file content")?, - }; - let current_deltas = deltas_reader - .read_file(&path) - .context("failed to get file deltas")?; - let mut text_doc = deltas::Document::new( - latest_file_content.as_ref(), - current_deltas.unwrap_or_default(), - )?; - let new_delta = text_doc - .update(current_wd_file_content.as_ref()) - .context("failed to calculate new deltas")?; - - let Some(new_delta) = new_delta else { - return Ok(false); - }; - - let deltas = text_doc.get_deltas(); - writer - .write(&path, &deltas) - .context("failed to write deltas")?; - - match ¤t_wd_file_content { - Some(reader::Content::UTF8(text)) => writer.write_wd_file(&path, text), - Some(_) => writer.write_wd_file(&path, ""), - None => writer.remove_wd_file(&path), - }?; - - let session_id = current_session.id; - self.emit_session_file(project_id, session_id, &path, latest_file_content)?; - self.index_deltas( - project_id, - session_id, - &path, - std::slice::from_ref(&new_delta), - ) - .context("failed to index deltas")?; - self.emit_app_event(Change::Deltas { - project_id, - session_id, - deltas: vec![new_delta], - relative_file_path: path, - })?; - Ok(true) - }; - Ok((process, session)) - }; - let num_paths = paths.len(); - let num_no_delta = std::thread::scope(|scope| -> Result { - let num_threads = std::thread::available_parallelism() - .unwrap_or(NonZeroUsize::new(1).unwrap()) - .get() - .min(paths.len()); - let mut num_no_delta = 0; - let current_session = if num_threads < 2 { - let (process, session) = make_processor()?; - for path in paths { - if !process(path)? { - num_no_delta += 1; - } - } - session - } else { - let (threads, tx) = { - let (tx, rx) = crossbeam_channel::bounded::(num_threads); - let threads: Vec<_> = (0..num_threads) - .map(|id| { - std::thread::Builder::new() - .name(format!("gitbutler_delta_thread_{id}")) - .stack_size(512 * 1024) - .spawn_scoped(scope, { - let rx = rx.clone(); - || -> Result { - let mut num_no_delta = 0; - let (process, _) = make_processor()?; - for path in rx { - if !process(path)? { - num_no_delta += 1; - } - } - Ok(num_no_delta) - } - }) - .expect("worker thread can be created") - }) - .collect(); - (threads, tx) - }; - for path in paths { - tx.send(path).expect("many receivers"); - } - drop(tx); - - for thread in threads { - num_no_delta += thread.join().unwrap()?; - } - let (_, session) = make_processor()?; - session - }; - self.index_session(project_id, current_session)?; - Ok(num_no_delta) - })?; - tracing::debug!(%project_id, paths_without_deltas = num_no_delta, paths_with_delta = num_paths - num_no_delta); - Ok(()) - } - - fn file_content( - project_repository: &project_repository::Repository, - path: &Path, - ) -> Result { - let full_path = project_repository.project().path.join(path); - if !full_path.exists() { - return Err(reader::Error::NotFound); - } - Ok(reader::Content::read_from_file(&full_path)?) - } -} diff --git a/crates/gitbutler-watcher/src/handler/index.rs b/crates/gitbutler-watcher/src/handler/index.rs deleted file mode 100644 index f17aca35f..000000000 --- a/crates/gitbutler-watcher/src/handler/index.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::path::Path; - -use anyhow::{Context, Result}; -use gitbutler_core::{ - deltas, gb_repository, project_repository, - projects::ProjectId, - sessions::{self, SessionId}, -}; - -use crate::Change; - -impl super::Handler { - pub(super) fn index_deltas( - &self, - project_id: ProjectId, - session_id: SessionId, - file_path: &Path, - deltas: &[deltas::Delta], - ) -> Result<()> { - self.deltas_db - .insert(&project_id, &session_id, file_path, deltas) - .context("failed to insert deltas into database") - } - - pub(crate) fn reindex(&self, project_id: ProjectId) -> Result<()> { - let user = self.users.get_user()?; - let project = self.projects.get(&project_id)?; - let project_repository = - project_repository::Repository::open(&project).context("failed to open repository")?; - let gb_repository = gb_repository::Repository::open( - &self.local_data_dir, - &project_repository, - user.as_ref(), - ) - .context("failed to open repository")?; - - let sessions_iter = gb_repository.get_sessions_iterator()?; - for session in sessions_iter { - self.process_session(&gb_repository, session?)?; - } - Ok(()) - } - - pub(super) fn index_session( - &self, - project_id: ProjectId, - session: sessions::Session, - ) -> Result<()> { - let project = self.projects.get(&project_id)?; - let project_repository = - project_repository::Repository::open(&project).context("failed to open repository")?; - let user = self.users.get_user()?; - let gb_repository = gb_repository::Repository::open( - &self.local_data_dir, - &project_repository, - user.as_ref(), - ) - .context("failed to open repository")?; - - self.process_session(&gb_repository, session) - } - - fn process_session( - &self, - gb_repository: &gb_repository::Repository, - session: sessions::Session, - ) -> Result<()> { - let project_id = gb_repository.get_project_id(); - - // now, index session if it has changed to the database. - let from_db = self.sessions_db.get_by_id(&session.id)?; - if from_db.map_or(false, |from_db| from_db == session) { - return Ok(()); - } - - self.sessions_db - .insert(project_id, &[&session]) - .context("failed to insert session into database")?; - - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - for (file_path, deltas) in deltas_reader - .read(None) - .context("could not list deltas for session")? - { - self.index_deltas(*project_id, session.id, &file_path, &deltas)?; - } - - (self.send_event)(Change::Session { - project_id: *project_id, - session, - })?; - Ok(()) - } -} diff --git a/crates/gitbutler-watcher/src/handler/mod.rs b/crates/gitbutler-watcher/src/handler/mod.rs index 4ef362b25..f7141bfc1 100644 --- a/crates/gitbutler-watcher/src/handler/mod.rs +++ b/crates/gitbutler-watcher/src/handler/mod.rs @@ -1,19 +1,12 @@ -mod calculate_deltas; -mod index; - -use std::path; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use anyhow::{Context, Result}; use gitbutler_core::ops::entry::{OperationType, SnapshotDetails}; use gitbutler_core::ops::oplog::Oplog; use gitbutler_core::projects::ProjectId; -use gitbutler_core::sessions::SessionId; use gitbutler_core::virtual_branches::VirtualBranches; -use gitbutler_core::{ - assets, deltas, git, project_repository, projects, reader, sessions, users, virtual_branches, -}; +use gitbutler_core::{assets, git, project_repository, projects, virtual_branches}; use tracing::instrument; use super::{events, Change}; @@ -28,13 +21,9 @@ pub struct Handler { // should be, and I can imagine having a top-level `app` handle that keeps the application state of // the tauri app, assuming that such application would not be `Send + Sync` everywhere and thus would // need extra protection. - users: users::Controller, - local_data_dir: path::PathBuf, projects: projects::Controller, vbranch_controller: virtual_branches::Controller, assets_proxy: assets::Proxy, - sessions_db: sessions::Database, - deltas_db: deltas::Database, /// A function to send events - decoupled from app-handle for testing purposes. #[allow(clippy::type_complexity)] @@ -45,23 +34,15 @@ impl Handler { /// A constructor whose primary use is the test-suite. #[allow(clippy::too_many_arguments)] pub fn new( - local_data_dir: PathBuf, - users: users::Controller, projects: projects::Controller, vbranch_controller: virtual_branches::Controller, assets_proxy: assets::Proxy, - sessions_db: sessions::Database, - deltas_db: deltas::Database, send_event: impl Fn(Change) -> Result<()> + Send + Sync + 'static, ) -> Self { Handler { - local_data_dir, - users, projects, vbranch_controller, assets_proxy, - sessions_db, - deltas_db, send_event: Arc::new(send_event), } } @@ -92,21 +73,6 @@ impl Handler { (self.send_event)(event).context("failed to send event") } - fn emit_session_file( - &self, - project_id: ProjectId, - session_id: SessionId, - file_path: &Path, - contents: Option, - ) -> Result<()> { - self.emit_app_event(Change::File { - project_id, - session_id, - file_path: file_path.to_owned(), - contents, - }) - } - #[instrument(skip(self, project_id))] async fn calculate_virtual_branches(&self, project_id: ProjectId) -> Result<()> { match self @@ -135,10 +101,6 @@ impl Handler { paths: Vec, project_id: ProjectId, ) -> Result<()> { - let calc_deltas = tokio::task::spawn_blocking({ - let this = self.clone(); - move || this.calculate_deltas(paths, project_id) - }); // Create a snapshot every time there are more than a configurable number of new lines of code (default 20) let handle_snapshots = tokio::task::spawn_blocking({ let this = self.clone(); @@ -146,7 +108,6 @@ impl Handler { }); self.calculate_virtual_branches(project_id).await?; let _ = handle_snapshots.await; - calc_deltas.await??; Ok(()) } diff --git a/crates/gitbutler-watcher/src/lib.rs b/crates/gitbutler-watcher/src/lib.rs index 70d28f07b..3fd77059a 100644 --- a/crates/gitbutler-watcher/src/lib.rs +++ b/crates/gitbutler-watcher/src/lib.rs @@ -75,7 +75,6 @@ pub fn watch_in_background( let (events_out, mut events_in) = unbounded_channel(); file_monitor::spawn(project_id, path.as_ref(), events_out.clone())?; - handler.reindex(project_id)?; let cancellation_token = CancellationToken::new(); let handle = WatcherHandle { diff --git a/crates/gitbutler-watcher/tests/handler/calculate_delta.rs b/crates/gitbutler-watcher/tests/handler/calculate_delta.rs deleted file mode 100644 index 425ad6580..000000000 --- a/crates/gitbutler-watcher/tests/handler/calculate_delta.rs +++ /dev/null @@ -1,982 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::atomic::{AtomicUsize, Ordering}, -}; - -use anyhow::Result; -use gitbutler_core::projects::ProjectId; -use gitbutler_core::{ - deltas::{self, operations::Operation}, - reader, sessions, - virtual_branches::{self, branch}, -}; -use once_cell::sync::Lazy; - -use self::branch::BranchId; -use crate::handler::support::Fixture; -use gitbutler_testsupport::{commit_all, Case}; - -static TEST_TARGET_INDEX: Lazy = Lazy::new(|| AtomicUsize::new(0)); - -#[derive(Clone)] -pub struct State { - inner: gitbutler_watcher::Handler, -} - -impl State { - pub(super) fn from_fixture(fixture: &mut Fixture) -> Self { - Self { - inner: fixture.new_handler(), - } - } - - pub(super) fn calculate_delta( - &self, - path: impl Into, - project_id: ProjectId, - ) -> Result<()> { - self.inner.calculate_deltas(vec![path.into()], project_id)?; - Ok(()) - } -} - -fn new_test_target() -> virtual_branches::target::Target { - virtual_branches::target::Target { - branch: format!( - "refs/remotes/remote name {}/branch name {}", - TEST_TARGET_INDEX.load(Ordering::Relaxed), - TEST_TARGET_INDEX.load(Ordering::Relaxed) - ) - .parse() - .unwrap(), - remote_url: format!("remote url {}", TEST_TARGET_INDEX.load(Ordering::Relaxed)), - sha: format!( - "0123456789abcdef0123456789abcdef0123456{}", - TEST_TARGET_INDEX.load(Ordering::Relaxed) - ) - .parse() - .unwrap(), - push_remote_name: None, - } -} - -static TEST_INDEX: Lazy = Lazy::new(|| AtomicUsize::new(0)); - -fn new_test_branch() -> branch::Branch { - TEST_INDEX.fetch_add(1, Ordering::Relaxed); - - branch::Branch { - id: BranchId::generate(), - name: format!("branch_name_{}", TEST_INDEX.load(Ordering::Relaxed)), - notes: format!("branch_notes_{}", TEST_INDEX.load(Ordering::Relaxed)), - applied: true, - upstream: Some( - format!( - "refs/remotes/origin/upstream_{}", - TEST_INDEX.load(Ordering::Relaxed) - ) - .parse() - .unwrap(), - ), - upstream_head: None, - created_timestamp_ms: TEST_INDEX.load(Ordering::Relaxed) as u128, - updated_timestamp_ms: (TEST_INDEX.load(Ordering::Relaxed) + 100) as u128, - head: format!( - "0123456789abcdef0123456789abcdef0123456{}", - TEST_INDEX.load(Ordering::Relaxed) - ) - .parse() - .unwrap(), - tree: format!( - "0123456789abcdef0123456789abcdef012345{}", - TEST_INDEX.load(Ordering::Relaxed) + 10 - ) - .parse() - .unwrap(), - ownership: branch::BranchOwnershipClaims::default(), - order: TEST_INDEX.load(Ordering::Relaxed), - selected_for_changes: None, - } -} - -#[test] -fn register_existing_commited_file() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case_with_files(HashMap::from([(PathBuf::from("test.txt"), "test")])); - - std::fs::write(project.path.join("test.txt"), "test2")?; - listener.calculate_delta("test.txt", project.id)?; - - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!( - deltas[0].operations[0], - Operation::Insert((4, "2".to_string())), - ); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "test2" - ); - - Ok(()) -} - -#[test] -fn register_must_init_current_session() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write(project.path.join("test.txt"), "test")?; - listener.calculate_delta("test.txt", project.id)?; - - assert!(gb_repository.get_current_session()?.is_some()); - - Ok(()) -} - -#[test] -fn register_must_not_override_current_session() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write(project.path.join("test.txt"), "test")?; - listener.calculate_delta("test.txt", project.id)?; - let session1 = gb_repository.get_current_session()?.unwrap(); - - std::fs::write(project.path.join("test.txt"), "test2")?; - listener.calculate_delta("test.txt", project.id)?; - let session2 = gb_repository.get_current_session()?.unwrap(); - - assert_eq!(session1.id, session2.id); - - Ok(()) -} - -#[test] -fn register_binfile() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write( - project.path.join("test.bin"), - [0, 159, 146, 150, 159, 146, 150], - )?; - - listener.calculate_delta("test.bin", project.id)?; - - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.bin")?.unwrap(); - - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 0); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.bin"))?, - "" - ); - - Ok(()) -} - -#[test] -fn register_empty_new_file() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write(project.path.join("test.txt"), "")?; - - listener.calculate_delta("test.txt", project.id)?; - - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 0); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "" - ); - - Ok(()) -} - -#[test] -fn register_new_file() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write(project.path.join("test.txt"), "test")?; - - listener.calculate_delta("test.txt", project.id)?; - - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!( - deltas[0].operations[0], - Operation::Insert((0, "test".to_string())), - ); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "test" - ); - - Ok(()) -} - -#[test] -fn register_no_changes_saved_thgoughout_flushes() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project_repository, - project, - .. - } = &fixture.new_case(); - - // file change, wd and deltas are written - std::fs::write(project.path.join("test.txt"), "test")?; - listener.calculate_delta("test.txt", project.id)?; - - // make two more sessions. - gb_repository.flush(project_repository, None)?; - gb_repository.get_or_create_current_session()?; - gb_repository.flush(project_repository, None)?; - - // after some sessions, files from the first change are still there. - let session = gb_repository.get_or_create_current_session()?; - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let files = session_reader.files(None)?; - assert_eq!(files.len(), 1); - - Ok(()) -} - -#[test] -fn register_new_file_twice() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - std::fs::write(project.path.join("test.txt"), "test")?; - listener.calculate_delta("test.txt", project.id)?; - - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!( - deltas[0].operations[0], - Operation::Insert((0, "test".to_string())), - ); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "test" - ); - - std::fs::write(project.path.join("test.txt"), "test2")?; - listener.calculate_delta("test.txt", project.id)?; - - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 2); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!( - deltas[0].operations[0], - Operation::Insert((0, "test".to_string())), - ); - assert_eq!(deltas[1].operations.len(), 1); - assert_eq!( - deltas[1].operations[0], - Operation::Insert((4, "2".to_string())), - ); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "test2" - ); - - Ok(()) -} - -#[test] -fn register_file_deleted() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project_repository, - project, - .. - } = &fixture.new_case(); - - { - // write file - std::fs::write(project.path.join("test.txt"), "test")?; - listener.calculate_delta("test.txt", project.id)?; - } - - { - // current session must have the deltas, but not the file (it didn't exist) - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!( - deltas[0].operations[0], - Operation::Insert((0, "test".to_string())), - ); - assert_eq!( - std::fs::read_to_string(gb_repository.session_wd_path().join("test.txt"))?, - "test" - ); - - let files = session_reader.files(None).unwrap(); - assert!(files.is_empty()); - } - - gb_repository.flush(project_repository, None)?; - - { - // file should be available in the next session, but not deltas just yet. - let session = gb_repository.get_or_create_current_session()?; - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let files = session_reader.files(None).unwrap(); - assert_eq!(files.len(), 1); - assert_eq!( - files[Path::new("test.txt")], - reader::Content::UTF8("test".to_string()) - ); - - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas = deltas_reader.read(None)?; - assert!(deltas.is_empty()); - - // removing the file - std::fs::remove_file(project.path.join("test.txt"))?; - listener.calculate_delta("test.txt", project.id)?; - - // deltas are recorded - let deltas = deltas_reader.read_file("test.txt")?.unwrap(); - assert_eq!(deltas.len(), 1); - assert_eq!(deltas[0].operations.len(), 1); - assert_eq!(deltas[0].operations[0], Operation::Delete((0, 4)),); - } - - gb_repository.flush(project_repository, None)?; - - { - // since file was deleted in the previous session, it should not exist in the new one. - let session = gb_repository.get_or_create_current_session()?; - let session_reader = sessions::Reader::open(gb_repository, &session)?; - let files = session_reader.files(None).unwrap(); - assert!(files.is_empty()); - } - - Ok(()) -} - -#[test] -fn flow_with_commits() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - project_repository, - .. - } = &fixture.new_case(); - - let size = 10; - let relative_file_path = Path::new("one/two/test.txt"); - for i in 1..=size { - std::fs::create_dir_all(Path::new(&project.path).join("one/two"))?; - // create a session with a single file change and flush it - std::fs::write( - Path::new(&project.path).join(relative_file_path), - i.to_string(), - )?; - - commit_all(&project_repository.git_repository); - listener.calculate_delta(relative_file_path, project.id)?; - assert!(gb_repository.flush(project_repository, None)?.is_some()); - } - - // get all the created sessions - let mut sessions: Vec = gb_repository - .get_sessions_iterator()? - .map(Result::unwrap) - .collect(); - assert_eq!(sessions.len(), size); - // verify sessions order is correct - let mut last_start = sessions[0].meta.start_timestamp_ms; - let mut last_end = sessions[0].meta.start_timestamp_ms; - sessions[1..].iter().for_each(|session| { - assert!(session.meta.start_timestamp_ms < last_start); - assert!(session.meta.last_timestamp_ms < last_end); - last_start = session.meta.start_timestamp_ms; - last_end = session.meta.last_timestamp_ms; - }); - - sessions.reverse(); - // try to reconstruct file state from operations for every session slice - for i in 0..sessions.len() { - let sessions_slice = &mut sessions[i..]; - - // collect all operations from sessions in the reverse order - let mut operations: Vec = vec![]; - for session in &mut *sessions_slice { - let session_reader = sessions::Reader::open(gb_repository, session).unwrap(); - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas_by_filepath = deltas_reader.read(None).unwrap(); - for deltas in deltas_by_filepath.values() { - for delta in deltas { - delta.operations.iter().for_each(|operation| { - operations.push(operation.clone()); - }); - } - } - } - - let reader = - sessions::Reader::open(gb_repository, sessions_slice.first().unwrap()).unwrap(); - let files = reader.files(None).unwrap(); - - if i == 0 { - assert_eq!(files.len(), 0); - } else { - assert_eq!(files.len(), 1); - } - - let base_file = files.get(&relative_file_path.to_path_buf()); - let mut text: Vec = match base_file { - Some(reader::Content::UTF8(file)) => file.chars().collect(), - _ => vec![], - }; - - for operation in operations { - operation.apply(&mut text).unwrap(); - } - - assert_eq!(text.iter().collect::(), size.to_string()); - } - Ok(()) -} - -#[test] -fn flow_no_commits() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - project_repository, - .. - } = &fixture.new_case(); - - let size = 10; - let relative_file_path = Path::new("one/two/test.txt"); - for i in 1..=size { - std::fs::create_dir_all(Path::new(&project.path).join("one/two"))?; - // create a session with a single file change and flush it - std::fs::write( - Path::new(&project.path).join(relative_file_path), - i.to_string(), - )?; - - listener.calculate_delta(relative_file_path, project.id)?; - assert!(gb_repository.flush(project_repository, None)?.is_some()); - } - - // get all the created sessions - let mut sessions: Vec = gb_repository - .get_sessions_iterator()? - .map(Result::unwrap) - .collect(); - assert_eq!(sessions.len(), size); - // verify sessions order is correct - let mut last_start = sessions[0].meta.start_timestamp_ms; - let mut last_end = sessions[0].meta.start_timestamp_ms; - sessions[1..].iter().for_each(|session| { - assert!(session.meta.start_timestamp_ms < last_start); - assert!(session.meta.last_timestamp_ms < last_end); - last_start = session.meta.start_timestamp_ms; - last_end = session.meta.last_timestamp_ms; - }); - - sessions.reverse(); - // try to reconstruct file state from operations for every session slice - for i in 0..sessions.len() { - let sessions_slice = &mut sessions[i..]; - - // collect all operations from sessions in the reverse order - let mut operations: Vec = vec![]; - for session in &mut *sessions_slice { - let session_reader = sessions::Reader::open(gb_repository, session).unwrap(); - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas_by_filepath = deltas_reader.read(None).unwrap(); - for deltas in deltas_by_filepath.values() { - for delta in deltas { - delta.operations.iter().for_each(|operation| { - operations.push(operation.clone()); - }); - } - } - } - - let reader = - sessions::Reader::open(gb_repository, sessions_slice.first().unwrap()).unwrap(); - let files = reader.files(None).unwrap(); - - if i == 0 { - assert_eq!(files.len(), 0); - } else { - assert_eq!(files.len(), 1); - } - - let base_file = files.get(&relative_file_path.to_path_buf()); - let mut text: Vec = match base_file { - Some(reader::Content::UTF8(file)) => file.chars().collect(), - _ => vec![], - }; - - for operation in operations { - operation.apply(&mut text).unwrap(); - } - - assert_eq!(text.iter().collect::(), size.to_string()); - } - Ok(()) -} - -#[test] -fn flow_signle_session() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - .. - } = &fixture.new_case(); - - let size = 10_i32; - let relative_file_path = Path::new("one/two/test.txt"); - for i in 1_i32..=size { - std::fs::create_dir_all(Path::new(&project.path).join("one/two"))?; - // create a session with a single file change and flush it - std::fs::write( - Path::new(&project.path).join(relative_file_path), - i.to_string(), - )?; - - listener.calculate_delta(relative_file_path, project.id)?; - } - - // collect all operations from sessions in the reverse order - let mut operations: Vec = vec![]; - let session = gb_repository.get_current_session()?.unwrap(); - let session_reader = sessions::Reader::open(gb_repository, &session).unwrap(); - let deltas_reader = deltas::Reader::new(&session_reader); - let deltas_by_filepath = deltas_reader.read(None).unwrap(); - for deltas in deltas_by_filepath.values() { - for delta in deltas { - delta.operations.iter().for_each(|operation| { - operations.push(operation.clone()); - }); - } - } - - let reader = sessions::Reader::open(gb_repository, &session).unwrap(); - let files = reader.files(None).unwrap(); - - let base_file = files.get(&relative_file_path.to_path_buf()); - let mut text: Vec = match base_file { - Some(reader::Content::UTF8(file)) => file.chars().collect(), - _ => vec![], - }; - - for operation in operations { - operation.apply(&mut text).unwrap(); - } - - assert_eq!(text.iter().collect::(), size.to_string()); - Ok(()) -} - -#[test] -fn should_persist_branches_targets_state_between_sessions() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - project, - project_repository, - .. - } = &fixture.new_case_with_files(HashMap::from([(PathBuf::from("test.txt"), "hello world")])); - - let vb_state = project.virtual_branches(); - let default_target = new_test_target(); - - vb_state.set_default_target(default_target.clone())?; - - let vbranch0 = new_test_branch(); - vb_state.set_branch(vbranch0.clone())?; - - let vbranch1 = new_test_branch(); - let vbranch1_target = new_test_target(); - vb_state.set_branch(vbranch1.clone())?; - vb_state.set_branch_target(vbranch1.id, vbranch1_target.clone())?; - - std::fs::write(project.path.join("test.txt"), "hello world!").unwrap(); - listener.calculate_delta("test.txt", project.id)?; - - let vb_state = project_repository.project().virtual_branches(); - - let branches = vb_state.list_branches().unwrap(); - assert_eq!(branches.len(), 2); - let branch_ids = branches.iter().map(|b| b.id).collect::>(); - assert!(branch_ids.contains(&vbranch0.id)); - assert!(branch_ids.contains(&vbranch1.id)); - - assert_eq!(vb_state.get_default_target().unwrap(), default_target); - assert_eq!( - vb_state.get_branch_target(&vbranch0.id).unwrap(), - default_target - ); - assert_eq!( - vb_state.get_branch_target(&vbranch1.id).unwrap(), - vbranch1_target - ); - - Ok(()) -} - -#[test] -fn should_restore_branches_targets_state_from_head_session() -> Result<()> { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { project, .. } = - &fixture.new_case_with_files(HashMap::from([(PathBuf::from("test.txt"), "hello world")])); - - let vb_state = project.virtual_branches(); - - let default_target = new_test_target(); - vb_state.set_default_target(default_target.clone())?; - let vbranch0 = new_test_branch(); - vb_state.set_branch(vbranch0.clone())?; - let vbranch1 = new_test_branch(); - let vbranch1_target = new_test_target(); - vb_state.set_branch(vbranch1.clone())?; - vb_state.set_branch_target(vbranch1.id, vbranch1_target.clone())?; - - std::fs::write(project.path.join("test.txt"), "hello world!").unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - - let branches = vb_state.list_branches().unwrap(); - assert_eq!(branches.len(), 2); - let branch_ids = branches.iter().map(|b| b.id).collect::>(); - assert!(branch_ids.contains(&vbranch0.id)); - assert!(branch_ids.contains(&vbranch1.id)); - - assert_eq!(vb_state.get_default_target().unwrap(), default_target); - assert_eq!( - vb_state.get_branch_target(&vbranch0.id).unwrap(), - default_target - ); - assert_eq!( - vb_state.get_branch_target(&vbranch1.id).unwrap(), - vbranch1_target - ); - - Ok(()) -} - -mod flush_wd { - use super::*; - - #[test] - fn should_add_new_files_to_session_wd() { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - project_repository, - .. - } = &fixture.new_case(); - - // write a file into session - std::fs::write(project.path.join("test.txt"), "hello world!").unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush it should be flushed into the commit - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert_eq!( - commit_reader.list_files(Path::new("wd")).unwrap(), - vec![Path::new("test.txt")] - ); - assert_eq!( - commit_reader.read(Path::new("wd/test.txt")).unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - } - - // write another file into session - std::fs::create_dir_all(project.path.join("one/two")).unwrap(); - std::fs::write(project.path.join("one/two/test2.txt"), "hello world!").unwrap(); - listener - .calculate_delta("one/two/test2.txt", project.id) - .unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush, it should be flushed into the commit next to the previous one - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert_eq!( - commit_reader.list_files(Path::new("wd")).unwrap(), - vec![Path::new("one/two/test2.txt"), Path::new("test.txt"),] - ); - assert_eq!( - commit_reader.read(Path::new("wd/test.txt")).unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - assert_eq!( - commit_reader - .read(Path::new("wd/one/two/test2.txt")) - .unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - } - } - - #[test] - fn should_remove_deleted_files_from_session_wd() { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - project_repository, - .. - } = &fixture.new_case(); - - // write a file into session - std::fs::write(project.path.join("test.txt"), "hello world!").unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - std::fs::create_dir_all(project.path.join("one/two")).unwrap(); - std::fs::write(project.path.join("one/two/test2.txt"), "hello world!").unwrap(); - listener - .calculate_delta("one/two/test2.txt", project.id) - .unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush it should be flushed into the commit - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert_eq!( - commit_reader.list_files(Path::new("wd")).unwrap(), - vec![Path::new("one/two/test2.txt"), Path::new("test.txt"),] - ); - assert_eq!( - commit_reader.read(Path::new("wd/test.txt")).unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - assert_eq!( - commit_reader - .read(Path::new("wd/one/two/test2.txt")) - .unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - } - - // rm the files - std::fs::remove_file(project.path.join("test.txt")).unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - std::fs::remove_file(project.path.join("one/two/test2.txt")).unwrap(); - listener - .calculate_delta("one/two/test2.txt", project.id) - .unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush it should be removed from the commit - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert!(commit_reader - .list_files(Path::new("wd")) - .unwrap() - .is_empty()); - } - } - - #[test] - fn should_update_updated_files_in_session_wd() { - let mut fixture = Fixture::default(); - let listener = State::from_fixture(&mut fixture); - let Case { - gb_repository, - project, - project_repository, - .. - } = &fixture.new_case(); - - // write a file into session - std::fs::write(project.path.join("test.txt"), "hello world!").unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - std::fs::create_dir_all(project.path.join("one/two")).unwrap(); - std::fs::write(project.path.join("one/two/test2.txt"), "hello world!").unwrap(); - listener - .calculate_delta("one/two/test2.txt", project.id) - .unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush it should be flushed into the commit - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert_eq!( - commit_reader.list_files(Path::new("wd")).unwrap(), - vec![Path::new("one/two/test2.txt"), Path::new("test.txt"),] - ); - assert_eq!( - commit_reader.read(Path::new("wd/test.txt")).unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - assert_eq!( - commit_reader - .read(Path::new("wd/one/two/test2.txt")) - .unwrap(), - reader::Content::UTF8("hello world!".to_string()) - ); - } - - // update the file - std::fs::write(project.path.join("test.txt"), "hello world!2").unwrap(); - listener.calculate_delta("test.txt", project.id).unwrap(); - - std::fs::write(project.path.join("one/two/test2.txt"), "hello world!2").unwrap(); - listener - .calculate_delta("one/two/test2.txt", project.id) - .unwrap(); - - let flushed_session = gb_repository - .flush(project_repository, None) - .unwrap() - .unwrap(); - { - // after flush it should be updated in the commit - let session_commit = gb_repository - .git_repository() - .find_commit(flushed_session.hash.unwrap()) - .unwrap(); - let commit_reader = - reader::Reader::from_commit(gb_repository.git_repository(), &session_commit) - .unwrap(); - assert_eq!( - commit_reader.list_files(Path::new("wd")).unwrap(), - vec![Path::new("one/two/test2.txt"), Path::new("test.txt"),] - ); - assert_eq!( - commit_reader.read(Path::new("wd/test.txt")).unwrap(), - reader::Content::UTF8("hello world!2".to_string()) - ); - assert_eq!( - commit_reader - .read(Path::new("wd/one/two/test2.txt")) - .unwrap(), - reader::Content::UTF8("hello world!2".to_string()) - ); - } - } -} diff --git a/crates/gitbutler-watcher/tests/handler/git_file_change.rs b/crates/gitbutler-watcher/tests/handler/git_file_change.rs deleted file mode 100644 index dfb55556e..000000000 --- a/crates/gitbutler-watcher/tests/handler/git_file_change.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::fs; - -use anyhow::Result; -use gitbutler_core::projects; - -use crate::handler::support::Fixture; -use gitbutler_testsupport::Case; -use gitbutler_watcher::Change; - -#[tokio::test] -async fn flush_session() -> Result<()> { - let mut fixture = Fixture::default(); - { - let case = fixture.new_case(); - let Case { - project, - gb_repository, - .. - } = &case; - - assert!(gb_repository.get_current_session()?.is_none()); - let handler = create_new_session_via_new_file(project, &mut fixture); - assert!(gb_repository.get_current_session()?.is_some()); - - let flush_file_path = project.path.join(".git/GB_FLUSH"); - fs::write(flush_file_path.as_path(), "")?; - - handler.git_file_change("GB_FLUSH", project.id).await?; - assert!(!flush_file_path.exists(), "flush file deleted"); - } - - let events = fixture.events(); - assert_eq!(events.len(), 4); - assert!(matches!(events[0], Change::File { .. })); - assert!(matches!(events[1], Change::Deltas { .. })); - assert!(matches!(events[2], Change::Session { .. })); - assert!(matches!(events[3], Change::Session { .. })); - Ok(()) -} - -#[tokio::test] -async fn do_not_flush_session_if_file_is_missing() -> Result<()> { - let mut fixture = Fixture::default(); - { - let Case { - project, - gb_repository, - .. - } = &fixture.new_case(); - - assert!(gb_repository.get_current_session()?.is_none()); - let handler = create_new_session_via_new_file(project, &mut fixture); - assert!(gb_repository.get_current_session()?.is_some()); - - handler.git_file_change("GB_FLUSH", project.id).await?; - } - let events = fixture.events(); - assert_eq!(events.len(), 3); - assert!(matches!(events[0], Change::File { .. })); - assert!(matches!(events[1], Change::Deltas { .. })); - assert!(matches!(events[2], Change::Session { .. })); - Ok(()) -} - -#[tokio::test] -async fn flush_deletes_flush_file_without_session_to_flush() -> Result<()> { - let mut fixture = Fixture::default(); - { - let handler = fixture.new_handler(); - let Case { project, .. } = &fixture.new_case(); - - let flush_file_path = project.path.join(".git/GB_FLUSH"); - fs::write(flush_file_path.as_path(), "")?; - - handler.git_file_change("GB_FLUSH", project.id).await?; - assert!(!flush_file_path.exists(), "flush file deleted"); - } - assert_eq!(fixture.events().len(), 0); - Ok(()) -} - -fn create_new_session_via_new_file( - project: &projects::Project, - fixture: &mut Fixture, -) -> gitbutler_watcher::Handler { - fs::write(project.path.join("test.txt"), "test").unwrap(); - - let handler = fixture.new_handler(); - handler - .calculate_deltas(vec!["test.txt".into()], project.id) - .unwrap(); - handler -} diff --git a/crates/gitbutler-watcher/tests/handler/mod.rs b/crates/gitbutler-watcher/tests/handler/mod.rs deleted file mode 100644 index 669ead06a..000000000 --- a/crates/gitbutler-watcher/tests/handler/mod.rs +++ /dev/null @@ -1,89 +0,0 @@ -mod support { - use gitbutler_core::{assets, deltas, git, sessions, virtual_branches}; - use tempfile::TempDir; - - /// Like [`gitbutler_testsupport::Suite`], but with all the instances needed to build a handler - pub struct Fixture { - inner: gitbutler_testsupport::Suite, - pub sessions_db: sessions::Database, - pub deltas_db: deltas::Database, - pub vbranch_controller: virtual_branches::Controller, - pub assets_proxy: assets::Proxy, - - /// Keeps changes emitted from the last created handler. - changes: Option>, - /// Storage for the databases, to be dropped last. - _tmp: TempDir, - } - - impl std::ops::Deref for Fixture { - type Target = gitbutler_testsupport::Suite; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl Default for Fixture { - fn default() -> Self { - let (db, tmp) = gitbutler_testsupport::test_database(); - let inner = gitbutler_testsupport::Suite::default(); - let sessions_db = sessions::Database::new(db.clone()); - let deltas_db = deltas::Database::new(db); - let git_credentials_helper = - git::credentials::Helper::new(inner.keys.clone(), inner.users.clone(), None); - let vbranch_controller = virtual_branches::Controller::new( - inner.projects.clone(), - inner.users.clone(), - inner.keys.clone(), - git_credentials_helper, - ); - let assets_proxy = assets::Proxy::new(tmp.path().to_owned()); - Fixture { - inner, - sessions_db, - deltas_db, - vbranch_controller, - assets_proxy, - changes: None, - _tmp: tmp, - } - } - } - - impl Fixture { - /// Must be mut as handler events are collected into the fixture automatically. - /// - /// Note that this only works for the most recent created handler. - pub fn new_handler(&mut self) -> gitbutler_watcher::Handler { - let (tx, rx) = std::sync::mpsc::channel(); - self.changes = Some(rx); - gitbutler_watcher::Handler::new( - self.local_app_data().to_owned(), - self.users.clone(), - self.projects.clone(), - self.vbranch_controller.clone(), - self.assets_proxy.clone(), - self.sessions_db.clone(), - self.deltas_db.clone(), - move |event| tx.send(event.clone()).map_err(Into::into), - ) - } - - /// Returns the events that were emitted to the tauri app. - pub fn events(&mut self) -> Vec { - let Some(rx) = self.changes.as_ref() else { - return Vec::new(); - }; - let mut out = Vec::new(); - // For safety, in case the `handler` is still alive, blocking consumption. - while let Ok(event) = rx.try_recv() { - out.push(event); - } - out - } - } -} - -mod calculate_delta; -mod git_file_change; diff --git a/crates/gitbutler-watcher/tests/watcher.rs b/crates/gitbutler-watcher/tests/watcher.rs deleted file mode 100644 index 427a47691..000000000 --- a/crates/gitbutler-watcher/tests/watcher.rs +++ /dev/null @@ -1 +0,0 @@ -mod handler;