Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions src-tauri/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,30 +26,48 @@ 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;
}
}

// 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)));
Expand All @@ -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();
Expand All @@ -77,21 +111,15 @@ 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;
}

*guard = Some(WatcherInner {
_watcher: watcher,
repo_root: repo_root.to_string(),
watch_path,
});
}
}
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down