diff --git a/src-tauri/src/watcher.rs b/src-tauri/src/watcher.rs index 4ac47c1..7f26328 100644 --- a/src-tauri/src/watcher.rs +++ b/src-tauri/src/watcher.rs @@ -11,6 +11,10 @@ pub struct WatcherState { struct WatcherInner { _watcher: RecommendedWatcher, repo_root: String, + /// The path actually being watched. When `.git/worktrees/` doesn't exist + /// yet we fall back to `.git/`; once the directory appears we want to + /// restart and watch it directly, so we track this to detect the upgrade. + watch_path: PathBuf, } impl WatcherState { @@ -22,14 +26,38 @@ impl WatcherState { /// Start watching the given repo's `.git/worktrees/` directory. /// If `.git/worktrees/` doesn't exist yet, watches `.git/` to detect its creation. - /// Skips restart if already watching the same repo (avoids event loops). + /// Skips restart if already watching the correct path (avoids event loops). pub fn start(&self, app: &AppHandle, repo_root: &str) { let mut guard = self.inner.lock().unwrap(); - // If already watching the same repo, don't restart — avoids triggering - // initial events that would cause an infinite refresh loop. + let git_dir = PathBuf::from(repo_root).join(".git"); + if !git_dir.is_dir() { + // Bare repo or not a git repo — skip watching + *guard = None; + return; + } + + // Use the canonical .git path so it matches the real paths FSEvents + // reports (e.g. /var vs /private/var on macOS); this keeps the path + // filter below reliable. + let git_dir = std::fs::canonicalize(&git_dir).unwrap_or(git_dir); + let worktrees_dir = git_dir.join("worktrees"); + + // Watch .git/worktrees/ if it exists, otherwise watch .git/ to detect + // its creation. Either way we only ever *react* to changes inside + // .git/worktrees/ (see the path filter in the callback) — never to the + // churn of unrelated .git files like index.lock, which grove's own + // `git status` calls create on every reload. + let watch_path = if worktrees_dir.is_dir() { + worktrees_dir.clone() + } else { + git_dir.clone() + }; + + // If already watching the correct path for this repo, don't restart — + // restarting can re-emit initial events and cause a refresh loop. if let Some(ref inner) = *guard { - if inner.repo_root == repo_root { + if inner.repo_root == repo_root && inner.watch_path == watch_path { return; } } @@ -37,15 +65,9 @@ impl WatcherState { // Stop existing watcher (dropped automatically) *guard = None; - let git_dir = PathBuf::from(repo_root).join(".git"); - if !git_dir.is_dir() { - // Bare repo or not a git repo — skip watching - return; - } - - let worktrees_dir = git_dir.join("worktrees"); let app_handle = app.clone(); let repo_root_owned = repo_root.to_string(); + let worktrees_filter = worktrees_dir.clone(); // Debounce: ignore events within 500ms of the last emitted event let last_emit = std::sync::Arc::new(Mutex::new(Instant::now() - std::time::Duration::from_secs(10))); @@ -59,6 +81,18 @@ impl WatcherState { _ => return, } + // Only react to changes to .git/worktrees/ itself or its contents + // (a worktree added/removed). Ignore everything else under .git/ — + // notably index.lock, which `git status` recreates on every reload + // and would otherwise drive an infinite refresh loop. + let relevant = event + .paths + .iter() + .any(|p| p.starts_with(&worktrees_filter)); + if !relevant { + return; + } + // If .git/worktrees/ was just created, we'll pick it up on next open_repo. // For now, just emit the change signal for any create/remove in watched dirs. let mut last = last_emit_clone.lock().unwrap(); @@ -77,14 +111,7 @@ impl WatcherState { } }; - // Watch .git/worktrees/ if it exists, otherwise watch .git/ to detect its creation - let watch_path = if worktrees_dir.is_dir() { - &worktrees_dir - } else { - &git_dir - }; - - if let Err(e) = watcher.watch(watch_path, RecursiveMode::NonRecursive) { + if let Err(e) = watcher.watch(&watch_path, RecursiveMode::NonRecursive) { eprintln!("[grove] failed to watch {}: {e}", watch_path.display()); return; } @@ -92,6 +119,7 @@ impl WatcherState { *guard = Some(WatcherInner { _watcher: watcher, repo_root: repo_root.to_string(), + watch_path, }); } } diff --git a/src/App.tsx b/src/App.tsx index 77afaae..e7f88e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -202,7 +202,7 @@ export default function App({ repoPath }: { repoPath: string }) { return () => { if (unlisten) unlisten(); }; - }); // re-subscribe when repo changes so closure captures latest repo + }, [repo?.repoRoot]); // re-subscribe when repo changes so closure captures latest repo const selectedWorktree = repo?.worktrees.find((w) => w.id === selectedWorktreeId) ?? null;