mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-18 17:35:15 +02:00
186 lines
5.8 KiB
Rust
186 lines
5.8 KiB
Rust
use crate::error::{Error, Result};
|
|
use chrono::Utc;
|
|
use log::{debug, error, warn};
|
|
use notify::Watcher;
|
|
use std::path::Path;
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
use tauri::{AppHandle, Listener, Runtime};
|
|
use tokio::select;
|
|
use tokio::sync::watch;
|
|
use tokio::time::sleep;
|
|
use yaak_git::{GitWorktreeStatus, git_path_is_ignored, git_repository_paths, git_worktree_status};
|
|
use yaak_rpc_schema::GitWatchResult;
|
|
|
|
const GIT_STATUS_COALESCE_WINDOW: Duration = Duration::from_millis(250);
|
|
|
|
pub(crate) async fn watch_git_worktree_status<R, F>(
|
|
app_handle: AppHandle<R>,
|
|
dir: &Path,
|
|
on_status: F,
|
|
) -> Result<GitWatchResult>
|
|
where
|
|
R: Runtime,
|
|
F: Fn(GitWorktreeStatus) + Send + Sync + 'static,
|
|
{
|
|
let paths = git_repository_paths(dir)?;
|
|
let repo_dir = dir.to_path_buf();
|
|
let workdir = paths.workdir;
|
|
let gitdir = paths.gitdir;
|
|
let commondir = paths.commondir;
|
|
|
|
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
|
let mut watcher = notify::recommended_watcher(tx)
|
|
.map_err(|e| Error::GenericError(format!("Failed to watch Git repository: {e}")))?;
|
|
|
|
// Watch only the directory Yaak syncs to, not the whole worktree — the
|
|
// containing repo may be huge and busy (e.g. a sync dir inside a monorepo),
|
|
// and watching it all burns CPU re-checking status for unrelated changes
|
|
watcher
|
|
.watch(&repo_dir, notify::RecursiveMode::Recursive)
|
|
.map_err(|e| Error::GenericError(format!("Failed to watch Git sync directory: {e}")))?;
|
|
|
|
// Watch the git metadata that affects branch/status info: the top-level
|
|
// gitdir files (HEAD, index) and refs. Not the whole gitdir, since
|
|
// .git/objects churns constantly during fetches and gc. Refs and
|
|
// packed-refs live in the common dir, which only differs from the gitdir
|
|
// for linked worktrees
|
|
watcher
|
|
.watch(&gitdir, notify::RecursiveMode::NonRecursive)
|
|
.map_err(|e| Error::GenericError(format!("Failed to watch Git metadata: {e}")))?;
|
|
if commondir != gitdir {
|
|
watcher
|
|
.watch(&commondir, notify::RecursiveMode::NonRecursive)
|
|
.map_err(|e| Error::GenericError(format!("Failed to watch Git common dir: {e}")))?;
|
|
}
|
|
let refs_dir = commondir.join("refs");
|
|
if refs_dir.exists() {
|
|
watcher
|
|
.watch(&refs_dir, notify::RecursiveMode::Recursive)
|
|
.map_err(|e| Error::GenericError(format!("Failed to watch Git refs: {e}")))?;
|
|
}
|
|
|
|
let (async_tx, mut async_rx) = tokio::sync::mpsc::channel::<notify::Result<notify::Event>>(100);
|
|
std::thread::spawn(move || {
|
|
for res in rx {
|
|
if async_tx.blocking_send(res).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
let (cancel_tx, cancel_rx) = watch::channel(());
|
|
let mut cancel_rx = cancel_rx;
|
|
send_worktree_status(&repo_dir, &on_status);
|
|
|
|
tauri::async_runtime::spawn(async move {
|
|
let _watcher = watcher;
|
|
loop {
|
|
select! {
|
|
Some(event_res) = async_rx.recv() => {
|
|
handle_git_watch_event(
|
|
event_res,
|
|
&mut async_rx,
|
|
&repo_dir,
|
|
&workdir,
|
|
&gitdir,
|
|
&commondir,
|
|
&on_status,
|
|
).await;
|
|
}
|
|
_ = cancel_rx.changed() => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let app_handle_inner = app_handle.clone();
|
|
let unlisten_event = format!("git-watch-unlisten-{}", Utc::now().timestamp_millis());
|
|
app_handle.listen_any(unlisten_event.clone(), move |event| {
|
|
app_handle_inner.unlisten(event.id());
|
|
if let Err(e) = cancel_tx.send(()) {
|
|
warn!("Failed to send git watch cancel signal {e:?}");
|
|
}
|
|
});
|
|
|
|
Ok(GitWatchResult { unlisten_event })
|
|
}
|
|
|
|
async fn handle_git_watch_event(
|
|
event_res: notify::Result<notify::Event>,
|
|
async_rx: &mut tokio::sync::mpsc::Receiver<notify::Result<notify::Event>>,
|
|
repo_dir: &Path,
|
|
workdir: &Path,
|
|
gitdir: &Path,
|
|
commondir: &Path,
|
|
on_status: &impl Fn(GitWorktreeStatus),
|
|
) {
|
|
if !is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir, commondir) {
|
|
return;
|
|
}
|
|
|
|
send_worktree_status(repo_dir, on_status);
|
|
|
|
let settle_window = sleep(GIT_STATUS_COALESCE_WINDOW);
|
|
tokio::pin!(settle_window);
|
|
loop {
|
|
select! {
|
|
Some(event_res) = async_rx.recv() => {
|
|
let _ = is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir, commondir);
|
|
}
|
|
_ = &mut settle_window => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
send_worktree_status(repo_dir, on_status);
|
|
}
|
|
|
|
fn is_relevant_git_watch_event(
|
|
event_res: notify::Result<notify::Event>,
|
|
repo_dir: &Path,
|
|
workdir: &Path,
|
|
gitdir: &Path,
|
|
commondir: &Path,
|
|
) -> bool {
|
|
let event = match event_res {
|
|
Ok(event) => event,
|
|
Err(e) => {
|
|
error!("Git watch error: {:?}", e);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
for path in event.paths {
|
|
if path.strip_prefix(gitdir).is_ok() || path.strip_prefix(commondir).is_ok() {
|
|
return true;
|
|
}
|
|
|
|
let Ok(rela_path) = path.strip_prefix(workdir) else {
|
|
continue;
|
|
};
|
|
|
|
match git_path_is_ignored(repo_dir, rela_path) {
|
|
Ok(true) => {}
|
|
Ok(false) => return true,
|
|
Err(e) => {
|
|
debug!("Failed to check Git ignore status for {:?}: {e}", rela_path);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn send_worktree_status(repo_dir: &Path, on_status: &impl Fn(GitWorktreeStatus)) {
|
|
match git_worktree_status(repo_dir) {
|
|
Ok(status) => on_status(status),
|
|
Err(e) => {
|
|
warn!("Failed to get git worktree status: {e}");
|
|
}
|
|
}
|
|
}
|