mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 11:45:06 +03:00
The watcher-crate compiles and tests run
This commit is contained in:
parent
143fc05547
commit
81dd1fc13e
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -2164,16 +2164,17 @@ name = "gitbutler-watcher"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"backoff",
|
||||
"crossbeam-channel",
|
||||
"futures",
|
||||
"git2",
|
||||
"gitbutler-analytics",
|
||||
"gitbutler-core",
|
||||
"gitbutler-testsupport",
|
||||
"itertools 0.12.1",
|
||||
"notify",
|
||||
"notify-debouncer-full",
|
||||
"tauri",
|
||||
"once_cell",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
@ -20,6 +20,7 @@ tokio = { version = "1.37.0" }
|
||||
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
gitbutler-core = { path = "crates/gitbutler-core" }
|
||||
gitbutler-analytics = { path = "crates/gitbutler-analytics" }
|
||||
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
|
||||
|
||||
[profile.release]
|
||||
|
@ -8,6 +8,7 @@ publish = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gitbutler-analytics.workspace = true
|
||||
gitbutler-core.workspace = true
|
||||
thiserror.workspace = true
|
||||
anyhow = "1.0.81"
|
||||
@ -15,7 +16,6 @@ futures = "0.3.30"
|
||||
tokio = { workspace = true, features = [ "full", "sync" ] }
|
||||
tokio-util = "0.7.10"
|
||||
tracing = "0.1.40"
|
||||
async-trait = "0.1.79"
|
||||
|
||||
backoff = "0.4.0"
|
||||
notify = { version = "6.0.1" }
|
||||
@ -23,19 +23,11 @@ notify-debouncer-full = "0.3.1"
|
||||
crossbeam-channel = "0.5.12"
|
||||
itertools = "0.12"
|
||||
|
||||
# TODO(ST): remove this dependency, abstract it
|
||||
[dependencies.tauri]
|
||||
version = "1.6.1"
|
||||
features = [
|
||||
"http-all", "os-all", "dialog-open", "fs-read-file",
|
||||
"path-all", "process-relaunch", "protocol-asset",
|
||||
"shell-open", "window-maximize", "window-start-dragging",
|
||||
"window-unmaximize"
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10"
|
||||
gitbutler-testsupport.workspace = true
|
||||
git2.workspace = true
|
||||
tempfile = "3.10"
|
||||
once_cell = "1.19"
|
||||
|
||||
[lints.clippy]
|
||||
all = "deny"
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::fmt::Display;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gitbutler_core::{deltas, reader, sessions::SessionId, virtual_branches};
|
||||
use gitbutler_core::{projects::ProjectId, sessions};
|
||||
|
||||
/// An event for internal use, as merge between [super::file_monitor::Event] and [Event].
|
||||
/// An event for internal use, as merge between [super::file_monitor::Event] and [Action].
|
||||
#[derive(Debug)]
|
||||
pub(super) enum InternalEvent {
|
||||
// From public API
|
||||
// From public action API
|
||||
Flush(ProjectId, sessions::Session),
|
||||
CalculateVirtualBranches(ProjectId),
|
||||
FetchGitbutlerData(ProjectId),
|
||||
@ -21,31 +22,33 @@ pub(super) enum InternalEvent {
|
||||
// TODO(ST): This should not have to be implemented in the Watcher, figure out how this can be moved
|
||||
// to application logic at least. However, it's called through a trait in `core`.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Event {
|
||||
#[allow(missing_docs)]
|
||||
pub enum Action {
|
||||
Flush(ProjectId, sessions::Session),
|
||||
CalculateVirtualBranches(ProjectId),
|
||||
FetchGitbutlerData(ProjectId),
|
||||
PushGitbutlerData(ProjectId),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
impl Action {
|
||||
/// Return the action's associated project id.
|
||||
pub fn project_id(&self) -> ProjectId {
|
||||
match self {
|
||||
Event::FetchGitbutlerData(project_id)
|
||||
| Event::Flush(project_id, _)
|
||||
| Event::CalculateVirtualBranches(project_id)
|
||||
| Event::PushGitbutlerData(project_id) => *project_id,
|
||||
Action::FetchGitbutlerData(project_id)
|
||||
| Action::Flush(project_id, _)
|
||||
| Action::CalculateVirtualBranches(project_id)
|
||||
| Action::PushGitbutlerData(project_id) => *project_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Event> for InternalEvent {
|
||||
fn from(value: Event) -> Self {
|
||||
impl From<Action> for InternalEvent {
|
||||
fn from(value: Action) -> Self {
|
||||
match value {
|
||||
Event::Flush(a, b) => InternalEvent::Flush(a, b),
|
||||
Event::CalculateVirtualBranches(v) => InternalEvent::CalculateVirtualBranches(v),
|
||||
Event::FetchGitbutlerData(v) => InternalEvent::FetchGitbutlerData(v),
|
||||
Event::PushGitbutlerData(v) => InternalEvent::PushGitbutlerData(v),
|
||||
Action::Flush(a, b) => InternalEvent::Flush(a, b),
|
||||
Action::CalculateVirtualBranches(v) => InternalEvent::CalculateVirtualBranches(v),
|
||||
Action::FetchGitbutlerData(v) => InternalEvent::FetchGitbutlerData(v),
|
||||
Action::PushGitbutlerData(v) => InternalEvent::PushGitbutlerData(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,3 +99,38 @@ fn comma_separated_paths(paths: &[PathBuf]) -> String {
|
||||
listing
|
||||
}
|
||||
}
|
||||
|
||||
/// An event telling the receiver something about the state of the application which just changed.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Change {
|
||||
GitIndex(ProjectId),
|
||||
GitFetch(ProjectId),
|
||||
GitHead {
|
||||
project_id: ProjectId,
|
||||
head: String,
|
||||
},
|
||||
GitActivity(ProjectId),
|
||||
File {
|
||||
project_id: ProjectId,
|
||||
session_id: SessionId,
|
||||
// TODO(ST): Should probably not be a string, but rather, relative.
|
||||
file_path: String,
|
||||
contents: Option<reader::Content>,
|
||||
},
|
||||
Session {
|
||||
project_id: ProjectId,
|
||||
session: sessions::Session,
|
||||
},
|
||||
Deltas {
|
||||
project_id: ProjectId,
|
||||
session_id: SessionId,
|
||||
// TODO(ST): check if this is ever more than one
|
||||
deltas: Vec<deltas::Delta>,
|
||||
relative_file_path: PathBuf,
|
||||
},
|
||||
VirtualBranches {
|
||||
project_id: ProjectId,
|
||||
virtual_branches: virtual_branches::VirtualBranches,
|
||||
},
|
||||
}
|
||||
|
@ -14,12 +14,12 @@ use gitbutler_core::{
|
||||
assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users,
|
||||
virtual_branches,
|
||||
};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::events;
|
||||
use crate::{analytics, events as app_events};
|
||||
use super::{events, Change};
|
||||
|
||||
/// A type that contains enough state to make decisions based on changes in the filesystem, which themselves
|
||||
/// may trigger [Changes](Change)
|
||||
// NOTE: This is `Clone` as each incoming event is spawned onto a thread for processing.
|
||||
#[derive(Clone)]
|
||||
pub struct Handler {
|
||||
@ -29,7 +29,7 @@ pub struct Handler {
|
||||
// the tauri app, assuming that such application would not be `Send + Sync` everywhere and thus would
|
||||
// need extra protection.
|
||||
users: users::Controller,
|
||||
analytics: analytics::Client,
|
||||
analytics: gitbutler_analytics::Client,
|
||||
local_data_dir: path::PathBuf,
|
||||
projects: projects::Controller,
|
||||
vbranch_controller: virtual_branches::Controller,
|
||||
@ -39,42 +39,7 @@ pub struct Handler {
|
||||
|
||||
/// A function to send events - decoupled from app-handle for testing purposes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
send_event: Arc<dyn Fn(&crate::events::Event) -> Result<()> + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub fn from_app(app: &AppHandle) -> Result<Self, anyhow::Error> {
|
||||
let app_data_dir = app
|
||||
.path_resolver()
|
||||
.app_data_dir()
|
||||
.context("failed to get app data dir")?;
|
||||
let analytics = app
|
||||
.try_state::<analytics::Client>()
|
||||
.map_or(analytics::Client::default(), |client| {
|
||||
client.inner().clone()
|
||||
});
|
||||
let users = app.state::<users::Controller>().inner().clone();
|
||||
let projects = app.state::<projects::Controller>().inner().clone();
|
||||
let vbranches = app.state::<virtual_branches::Controller>().inner().clone();
|
||||
let assets_proxy = app.state::<assets::Proxy>().inner().clone();
|
||||
let sessions_db = app.state::<sessions::Database>().inner().clone();
|
||||
let deltas_db = app.state::<deltas::Database>().inner().clone();
|
||||
|
||||
Ok(Handler::new(
|
||||
app_data_dir.clone(),
|
||||
analytics,
|
||||
users,
|
||||
projects,
|
||||
vbranches,
|
||||
assets_proxy,
|
||||
sessions_db,
|
||||
deltas_db,
|
||||
{
|
||||
let app = app.clone();
|
||||
move |event: &crate::events::Event| event.send(&app)
|
||||
},
|
||||
))
|
||||
}
|
||||
send_event: Arc<dyn Fn(Change) -> Result<()> + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
@ -82,14 +47,14 @@ impl Handler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
local_data_dir: PathBuf,
|
||||
analytics: analytics::Client,
|
||||
analytics: gitbutler_analytics::Client,
|
||||
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(&app_events::Event) -> Result<()> + Send + Sync + 'static,
|
||||
send_event: impl Fn(Change) -> Result<()> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Handler {
|
||||
local_data_dir,
|
||||
@ -144,7 +109,7 @@ impl Handler {
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn emit_app_event(&self, event: &crate::events::Event) -> Result<()> {
|
||||
fn emit_app_event(&self, event: Change) -> Result<()> {
|
||||
(self.send_event)(event).context("failed to send event")
|
||||
}
|
||||
|
||||
@ -153,16 +118,16 @@ impl Handler {
|
||||
project_id: ProjectId,
|
||||
session_id: SessionId,
|
||||
file_path: &Path,
|
||||
contents: Option<&reader::Content>,
|
||||
contents: Option<reader::Content>,
|
||||
) -> Result<()> {
|
||||
self.emit_app_event(&app_events::Event::file(
|
||||
self.emit_app_event(Change::File {
|
||||
project_id,
|
||||
session_id,
|
||||
&file_path.display().to_string(),
|
||||
file_path: file_path.display().to_string(),
|
||||
contents,
|
||||
))
|
||||
})
|
||||
}
|
||||
fn send_analytics_event_none_blocking(&self, event: &analytics::Event) -> Result<()> {
|
||||
fn send_analytics_event_none_blocking(&self, event: &gitbutler_analytics::Event) -> Result<()> {
|
||||
if let Some(user) = self.users.get_user().context("failed to get user")? {
|
||||
self.analytics
|
||||
.send_non_anonymous_event_nonblocking(&user, event);
|
||||
@ -193,7 +158,7 @@ impl Handler {
|
||||
.flush_session(&project_repository, session, user.as_ref())
|
||||
.context(format!("failed to flush session {}", session.id))?;
|
||||
|
||||
self.index_session(project_id, &session)?;
|
||||
self.index_session(project_id, session)?;
|
||||
|
||||
let push_gb_data = tokio::task::spawn_blocking({
|
||||
let this = self.clone();
|
||||
@ -213,13 +178,13 @@ impl Handler {
|
||||
{
|
||||
Ok((branches, skipped_files)) => {
|
||||
let branches = self.assets_proxy.proxy_virtual_branches(branches).await;
|
||||
self.emit_app_event(&app_events::Event::virtual_branches(
|
||||
self.emit_app_event(Change::VirtualBranches {
|
||||
project_id,
|
||||
&VirtualBranches {
|
||||
virtual_branches: VirtualBranches {
|
||||
branches,
|
||||
skipped_files,
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
Err(err) if err.is::<virtual_branches::errors::VerifyError>() => Ok(()),
|
||||
Err(err) => Err(err.context("failed to list virtual branches").into()),
|
||||
@ -314,7 +279,7 @@ impl Handler {
|
||||
let sessions_after_fetch = gb_repo.get_sessions_iterator()?.filter_map(Result::ok);
|
||||
let new_sessions = sessions_after_fetch.filter(|s| !sessions_before_fetch.contains(s));
|
||||
for session in new_sessions {
|
||||
self.index_session(project_id, &session)?;
|
||||
self.index_session(project_id, session)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -358,11 +323,11 @@ impl Handler {
|
||||
};
|
||||
match file_name {
|
||||
"FETCH_HEAD" => {
|
||||
self.emit_app_event(&app_events::Event::git_fetch(project_id))?;
|
||||
self.emit_app_event(Change::GitFetch(project_id))?;
|
||||
self.calculate_virtual_branches(project_id).await?;
|
||||
}
|
||||
"logs/HEAD" => {
|
||||
self.emit_app_event(&app_events::Event::git_activity(project.id))?;
|
||||
self.emit_app_event(Change::GitActivity(project.id))?;
|
||||
}
|
||||
"GB_FLUSH" => {
|
||||
let user = self.users.get_user()?;
|
||||
@ -404,18 +369,20 @@ impl Handler {
|
||||
integration_reference.delete()?;
|
||||
}
|
||||
if let Some(head) = head_ref.name() {
|
||||
self.send_analytics_event_none_blocking(&analytics::Event::HeadChange {
|
||||
self.send_analytics_event_none_blocking(
|
||||
&gitbutler_analytics::Event::HeadChange {
|
||||
project_id,
|
||||
reference_name: head_ref_name.to_string(),
|
||||
},
|
||||
)?;
|
||||
self.emit_app_event(Change::GitHead {
|
||||
project_id,
|
||||
reference_name: head_ref_name.to_string(),
|
||||
head: head.to_string(),
|
||||
})?;
|
||||
self.emit_app_event(&app_events::Event::git_head(
|
||||
project_id,
|
||||
&head.to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
"index" => {
|
||||
self.emit_app_event(&app_events::Event::git_index(project.id))?;
|
||||
self.emit_app_event(Change::GitIndex(project.id))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::Change;
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_core::{
|
||||
deltas, gb_repository, project_repository, projects::ProjectId, reader, sessions,
|
||||
@ -43,7 +44,7 @@ impl super::Handler {
|
||||
.context("failed to get or create current session")?;
|
||||
let session = current_session.clone();
|
||||
|
||||
let process = move |path: &Path| -> Result<bool> {
|
||||
let process = move |path: PathBuf| -> Result<bool> {
|
||||
let _span = tracing::span!(tracing::Level::TRACE, "processing", ?path).entered();
|
||||
let current_session_reader =
|
||||
sessions::Reader::open(&gb_repository, ¤t_session)
|
||||
@ -51,18 +52,18 @@ impl super::Handler {
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
.read_file(&path)
|
||||
.context("failed to get file deltas")?;
|
||||
let mut text_doc = deltas::Document::new(
|
||||
latest_file_content.as_ref(),
|
||||
@ -78,30 +79,30 @@ impl super::Handler {
|
||||
|
||||
let deltas = text_doc.get_deltas();
|
||||
writer
|
||||
.write(path, &deltas)
|
||||
.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),
|
||||
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.as_ref())?;
|
||||
self.emit_session_file(project_id, session_id, &path, latest_file_content)?;
|
||||
self.index_deltas(
|
||||
project_id,
|
||||
session_id,
|
||||
path,
|
||||
&path,
|
||||
std::slice::from_ref(&new_delta),
|
||||
)
|
||||
.context("failed to index deltas")?;
|
||||
self.emit_app_event(&app_events::Event::deltas(
|
||||
self.emit_app_event(Change::Deltas {
|
||||
project_id,
|
||||
session_id,
|
||||
std::slice::from_ref(&new_delta),
|
||||
path,
|
||||
))?;
|
||||
deltas: vec![new_delta],
|
||||
relative_file_path: path,
|
||||
})?;
|
||||
Ok(true)
|
||||
};
|
||||
Ok((process, session))
|
||||
@ -116,7 +117,7 @@ impl super::Handler {
|
||||
let current_session = if num_threads < 2 {
|
||||
let (process, session) = make_processor()?;
|
||||
for path in paths {
|
||||
if !process(path.as_path())? {
|
||||
if !process(path)? {
|
||||
num_no_delta += 1;
|
||||
}
|
||||
}
|
||||
@ -135,7 +136,7 @@ impl super::Handler {
|
||||
let mut num_no_delta = 0;
|
||||
let (process, _) = make_processor()?;
|
||||
for path in rx {
|
||||
if !process(path.as_path())? {
|
||||
if !process(path)? {
|
||||
num_no_delta += 1;
|
||||
}
|
||||
}
|
||||
@ -158,7 +159,7 @@ impl super::Handler {
|
||||
let (_, session) = make_processor()?;
|
||||
session
|
||||
};
|
||||
self.index_session(project_id, ¤t_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);
|
||||
|
@ -7,7 +7,7 @@ use gitbutler_core::{
|
||||
sessions::{self, SessionId},
|
||||
};
|
||||
|
||||
use crate::events as app_events;
|
||||
use crate::Change;
|
||||
|
||||
impl super::Handler {
|
||||
pub(super) fn index_deltas(
|
||||
@ -36,7 +36,7 @@ impl super::Handler {
|
||||
|
||||
let sessions_iter = gb_repository.get_sessions_iterator()?;
|
||||
for session in sessions_iter {
|
||||
self.process_session(&gb_repository, &session?)?;
|
||||
self.process_session(&gb_repository, session?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -44,7 +44,7 @@ impl super::Handler {
|
||||
pub(super) fn index_session(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
session: &sessions::Session,
|
||||
session: sessions::Session,
|
||||
) -> Result<()> {
|
||||
let project = self.projects.get(&project_id)?;
|
||||
let project_repository =
|
||||
@ -63,21 +63,21 @@ impl super::Handler {
|
||||
fn process_session(
|
||||
&self,
|
||||
gb_repository: &gb_repository::Repository,
|
||||
session: &sessions::Session,
|
||||
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) {
|
||||
if from_db.map_or(false, |from_db| from_db == session) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.sessions_db
|
||||
.insert(project_id, &[session])
|
||||
.insert(project_id, &[&session])
|
||||
.context("failed to insert session into database")?;
|
||||
|
||||
let session_reader = sessions::Reader::open(gb_repository, session)?;
|
||||
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)
|
||||
@ -86,7 +86,10 @@ impl super::Handler {
|
||||
self.index_deltas(*project_id, session.id, &file_path, &deltas)?;
|
||||
}
|
||||
|
||||
(self.send_event)(&app_events::Event::session(*project_id, session))?;
|
||||
(self.send_event)(Change::Session {
|
||||
project_id: *project_id,
|
||||
session,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,104 +1,29 @@
|
||||
//! Implement the file-monitoring agent that informs about changes in interesting locations.
|
||||
#![deny(missing_docs)]
|
||||
#![deny(unsafe_code, rust_2018_idioms)]
|
||||
#![allow(clippy::doc_markdown, clippy::missing_errors_doc)]
|
||||
#![feature(slice_as_chunks)]
|
||||
|
||||
mod events;
|
||||
pub use events::Event;
|
||||
use events::InternalEvent;
|
||||
pub use events::{Action, Change};
|
||||
|
||||
mod file_monitor;
|
||||
mod handler;
|
||||
pub use handler::Handler;
|
||||
|
||||
use std::path::Path;
|
||||
use std::{sync::Arc, time};
|
||||
use std::time;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures::executor::block_on;
|
||||
use gitbutler_core::projects::{self, Project, ProjectId};
|
||||
use tauri::AppHandle;
|
||||
use gitbutler_core::projects::ProjectId;
|
||||
use tokio::{
|
||||
sync::mpsc::{unbounded_channel, UnboundedSender},
|
||||
task,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::instrument;
|
||||
|
||||
/// Note that this type is managed in Tauri and thus needs to be send and sync.
|
||||
#[derive(Clone)]
|
||||
pub struct Watchers {
|
||||
/// NOTE: This handle is required for this type to be self-contained as it's used by `core` through a trait.
|
||||
app_handle: AppHandle,
|
||||
/// The watcher of the currently active project.
|
||||
/// NOTE: This is a `tokio` mutex as this needs to lock the inner option from within async.
|
||||
watcher: Arc<tokio::sync::Mutex<Option<WatcherHandle>>>,
|
||||
}
|
||||
|
||||
impl Watchers {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
Self {
|
||||
app_handle,
|
||||
watcher: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, project), err(Debug))]
|
||||
pub fn watch(&self, project: &projects::Project) -> Result<()> {
|
||||
let handler = handler::Handler::from_app(&self.app_handle)?;
|
||||
|
||||
let project_id = project.id;
|
||||
let project_path = project.path.clone();
|
||||
|
||||
let handle = watch_in_background(handler, project_path, project_id)?;
|
||||
block_on(self.watcher.lock()).replace(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post(&self, event: Event) -> Result<()> {
|
||||
let watcher = self.watcher.lock().await;
|
||||
if let Some(handle) = watcher
|
||||
.as_ref()
|
||||
.filter(|watcher| watcher.project_id == event.project_id())
|
||||
{
|
||||
handle.post(event).await.context("failed to post event")
|
||||
} else {
|
||||
Err(anyhow::anyhow!("watcher not found",))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(&self, project_id: ProjectId) {
|
||||
let mut handle = self.watcher.lock().await;
|
||||
if handle
|
||||
.as_ref()
|
||||
.map_or(false, |handle| handle.project_id == project_id)
|
||||
{
|
||||
handle.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl gitbutler_core::projects::Watchers for Watchers {
|
||||
fn watch(&self, project: &Project) -> Result<()> {
|
||||
Watchers::watch(self, project)
|
||||
}
|
||||
|
||||
async fn stop(&self, id: ProjectId) {
|
||||
Watchers::stop(self, id).await
|
||||
}
|
||||
|
||||
async fn fetch_gb_data(&self, id: ProjectId) -> Result<()> {
|
||||
self.post(Event::FetchGitbutlerData(id)).await
|
||||
}
|
||||
|
||||
async fn push_gb_data(&self, id: ProjectId) -> Result<()> {
|
||||
self.post(Event::PushGitbutlerData(id)).await
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstraction over a link to the spawned watcher, which runs in the background.
|
||||
struct WatcherHandle {
|
||||
pub struct WatcherHandle {
|
||||
/// A way to post events and interact with the actual handler in the background.
|
||||
tx: UnboundedSender<InternalEvent>,
|
||||
/// The id of the project we are watching.
|
||||
@ -114,10 +39,18 @@ impl Drop for WatcherHandle {
|
||||
}
|
||||
|
||||
impl WatcherHandle {
|
||||
pub async fn post(&self, event: Event) -> Result<()> {
|
||||
self.tx.send(event.into()).context("failed to send event")?;
|
||||
/// Post an `action` for the watcher to perform.
|
||||
pub async fn post(&self, action: Action) -> Result<()> {
|
||||
self.tx
|
||||
.send(action.into())
|
||||
.context("failed to send event")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the id of the project we are watching.
|
||||
pub fn project_id(&self) -> ProjectId {
|
||||
self.project_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Run our file watcher processing loop in the background and let `handler` deal with them.
|
||||
@ -128,7 +61,7 @@ impl WatcherHandle {
|
||||
///
|
||||
/// It runs in such a way that each filesystem event is processed concurrently with others, which is why
|
||||
/// spamming massive amounts of events should be avoided!
|
||||
fn watch_in_background(
|
||||
pub fn watch_in_background(
|
||||
handler: handler::Handler,
|
||||
path: impl AsRef<Path>,
|
||||
project_id: ProjectId,
|
||||
|
@ -11,18 +11,17 @@ use gitbutler_core::{
|
||||
reader, sessions,
|
||||
virtual_branches::{self, branch, VirtualBranchesHandle},
|
||||
};
|
||||
use gitbutler_tauri::watcher;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use self::branch::BranchId;
|
||||
use crate::watcher::handler::support::Fixture;
|
||||
use crate::handler::support::Fixture;
|
||||
use gitbutler_testsupport::{commit_all, Case};
|
||||
|
||||
static TEST_TARGET_INDEX: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(0));
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct State {
|
||||
inner: watcher::Handler,
|
||||
inner: gitbutler_watcher::Handler,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
@ -1,10 +1,9 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use gitbutler_core::projects;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::watcher::handler::support::Fixture;
|
||||
use crate::watcher::handler::test_remote_repository;
|
||||
use crate::handler::support::Fixture;
|
||||
use crate::handler::test_remote_repository;
|
||||
use gitbutler_testsupport::Case;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -2,11 +2,10 @@ use std::fs;
|
||||
|
||||
use anyhow::Result;
|
||||
use gitbutler_core::projects;
|
||||
use gitbutler_tauri::watcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::watcher::handler::support::Fixture;
|
||||
use crate::handler::support::Fixture;
|
||||
use gitbutler_testsupport::Case;
|
||||
use gitbutler_watcher::Change;
|
||||
|
||||
#[tokio::test]
|
||||
async fn flush_session() -> Result<()> {
|
||||
@ -32,10 +31,10 @@ async fn flush_session() -> Result<()> {
|
||||
|
||||
let events = fixture.events();
|
||||
assert_eq!(events.len(), 4);
|
||||
assert!(events[0].name().ends_with("/files"));
|
||||
assert!(events[1].name().ends_with("/deltas"));
|
||||
assert!(events[2].name().ends_with("/sessions"));
|
||||
assert!(events[3].name().ends_with("/sessions"));
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -57,9 +56,9 @@ async fn do_not_flush_session_if_file_is_missing() -> Result<()> {
|
||||
}
|
||||
let events = fixture.events();
|
||||
assert_eq!(events.len(), 3);
|
||||
assert!(events[0].name().ends_with("/files"));
|
||||
assert!(events[1].name().ends_with("/deltas"));
|
||||
assert!(events[2].name().ends_with("/sessions"));
|
||||
assert!(matches!(events[0], Change::File { .. }));
|
||||
assert!(matches!(events[1], Change::Deltas { .. }));
|
||||
assert!(matches!(events[2], Change::Session { .. }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -83,7 +82,7 @@ async fn flush_deletes_flush_file_without_session_to_flush() -> Result<()> {
|
||||
fn create_new_session_via_new_file(
|
||||
project: &projects::Project,
|
||||
fixture: &mut Fixture,
|
||||
) -> watcher::Handler {
|
||||
) -> gitbutler_watcher::Handler {
|
||||
fs::write(project.path.join("test.txt"), "test").unwrap();
|
||||
|
||||
let handler = fixture.new_handler();
|
||||
|
@ -2,7 +2,6 @@ use tempfile::TempDir;
|
||||
|
||||
mod support {
|
||||
use gitbutler_core::{assets, deltas, git, sessions, virtual_branches};
|
||||
use gitbutler_tauri::{analytics, watcher};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Like [`gitbutler_testsupport::Suite`], but with all the instances needed to build a handler
|
||||
@ -13,8 +12,8 @@ mod support {
|
||||
pub vbranch_controller: virtual_branches::Controller,
|
||||
pub assets_proxy: assets::Proxy,
|
||||
|
||||
/// Keeps events emitted from the last created handler.
|
||||
events: Option<std::sync::mpsc::Receiver<gitbutler_tauri::Event>>,
|
||||
/// Keeps changes emitted from the last created handler.
|
||||
changes: Option<std::sync::mpsc::Receiver<gitbutler_watcher::Change>>,
|
||||
/// Storage for the databases, to be dropped last.
|
||||
_tmp: TempDir,
|
||||
}
|
||||
@ -49,7 +48,7 @@ mod support {
|
||||
deltas_db,
|
||||
vbranch_controller,
|
||||
assets_proxy,
|
||||
events: None,
|
||||
changes: None,
|
||||
_tmp: tmp,
|
||||
}
|
||||
}
|
||||
@ -59,12 +58,12 @@ mod support {
|
||||
/// 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) -> watcher::Handler {
|
||||
pub fn new_handler(&mut self) -> gitbutler_watcher::Handler {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.events = Some(rx);
|
||||
watcher::Handler::new(
|
||||
self.changes = Some(rx);
|
||||
gitbutler_watcher::Handler::new(
|
||||
self.local_app_data().to_owned(),
|
||||
analytics::Client::default(),
|
||||
gitbutler_analytics::Client::default(),
|
||||
self.users.clone(),
|
||||
self.projects.clone(),
|
||||
self.vbranch_controller.clone(),
|
||||
@ -76,8 +75,8 @@ mod support {
|
||||
}
|
||||
|
||||
/// Returns the events that were emitted to the tauri app.
|
||||
pub fn events(&mut self) -> Vec<gitbutler_tauri::Event> {
|
||||
let Some(rx) = self.events.as_ref() else {
|
||||
pub fn events(&mut self) -> Vec<gitbutler_watcher::Change> {
|
||||
let Some(rx) = self.changes.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
|
@ -3,8 +3,8 @@ use std::{collections::HashMap, path::PathBuf};
|
||||
use anyhow::Result;
|
||||
use gitbutler_core::{git, project_repository::LogUntil, projects};
|
||||
|
||||
use crate::watcher::handler::support::Fixture;
|
||||
use crate::watcher::handler::test_remote_repository;
|
||||
use crate::handler::support::Fixture;
|
||||
use crate::handler::test_remote_repository;
|
||||
use gitbutler_testsupport::{virtual_branches::set_test_target, Case};
|
||||
|
||||
fn log_walk(repo: &git2::Repository, head: git::Oid) -> Vec<git::Oid> {
|
||||
|
Loading…
Reference in New Issue
Block a user