Fix Linux watcher cleanup for recreated directories (#50412)

## Problem
- On Linux, non-recursive watcher registrations remained path-cached
after deleting and recreating a directory in the same session.
- The recreated directory was not re-watched, so newly created child
entries under that path could be missing.

## Summary
- Remove directory watcher registrations when worktree paths are removed
from snapshot state.
- Ensure recreated directories can be watched again on Linux by allowing
`scan_dir` to re-add fresh watches.
- Add a Linux integration regression test for directory delete/recreate
path reuse and child file creation.

## Testing
- `cargo test -p project --features test-support --test integration
test_recreated_directory_receives_child_events -- --exact`
- `cargo test -p project --features test-support --test integration
test_rescan_and_remote_updates -- --exact`

## Related
- #46709

Release Notes:

- Fixed Linux worktree file watching so child entries appear after
deleting and recreating a directory at the same path.
This commit is contained in:
Chriss4123 2026-03-03 18:11:51 +02:00 committed by GitHub
parent 38c7e63af3
commit c19cc4c51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 61 additions and 3 deletions

View file

@ -5359,6 +5359,52 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
});
}
#[cfg(target_os = "linux")]
#[gpui::test(retries = 5)]
async fn test_recreated_directory_receives_child_events(cx: &mut gpui::TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let dir = TempTree::new(json!({}));
let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
tree.flush_fs_events(cx).await;
let repro_dir = dir.path().join("repro");
std::fs::create_dir(&repro_dir).unwrap();
tree.flush_fs_events(cx).await;
cx.update(|cx| {
assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_some());
});
std::fs::remove_dir_all(&repro_dir).unwrap();
tree.flush_fs_events(cx).await;
cx.update(|cx| {
assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_none());
});
std::fs::create_dir(&repro_dir).unwrap();
tree.flush_fs_events(cx).await;
cx.update(|cx| {
assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_some());
});
std::fs::write(repro_dir.join("repro-marker"), "").unwrap();
tree.flush_fs_events(cx).await;
cx.update(|cx| {
assert!(
tree.read(cx)
.entry_for_path(rel_path("repro/repro-marker"))
.is_some()
);
});
}
#[gpui::test(iterations = 10)]
async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
init_test(cx);

View file

@ -2945,7 +2945,7 @@ impl BackgroundScannerState {
self.snapshot.check_invariants(false);
}
fn remove_path(&mut self, path: &RelPath) {
fn remove_path(&mut self, path: &RelPath, watcher: &dyn Watcher) {
log::trace!("background scanner removing path {path:?}");
let mut new_entries;
let removed_entries;
@ -2961,7 +2961,12 @@ impl BackgroundScannerState {
self.snapshot.entries_by_path = new_entries;
let mut removed_ids = Vec::with_capacity(removed_entries.summary().count);
let mut removed_dir_abs_paths = Vec::new();
for entry in removed_entries.cursor::<()>(()) {
if entry.is_dir() {
removed_dir_abs_paths.push(self.snapshot.absolutize(&entry.path));
}
match self.removed_entries.entry(entry.inode) {
hash_map::Entry::Occupied(mut e) => {
let prev_removed_entry = e.get_mut();
@ -2997,6 +3002,10 @@ impl BackgroundScannerState {
.git_repositories
.retain(|id, _| removed_ids.binary_search(id).is_err());
for removed_dir_abs_path in removed_dir_abs_paths {
watcher.remove(&removed_dir_abs_path).log_err();
}
#[cfg(feature = "test-support")]
self.snapshot.check_invariants(false);
}
@ -4461,7 +4470,10 @@ impl BackgroundScanner {
if self.settings.is_path_excluded(&child_path) {
log::debug!("skipping excluded child entry {child_path:?}");
self.state.lock().await.remove_path(&child_path);
self.state
.lock()
.await
.remove_path(&child_path, self.watcher.as_ref());
continue;
}
@ -4651,7 +4663,7 @@ impl BackgroundScanner {
// detected regardless of the order of the paths.
for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
if matches!(metadata, Ok(None)) || doing_recursive_update {
state.remove_path(path);
state.remove_path(path, self.watcher.as_ref());
}
}