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( app_handle: AppHandle, dir: &Path, on_status: F, ) -> Result 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::>(); 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::>(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, async_rx: &mut tokio::sync::mpsc::Receiver>, 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, 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}"); } } }