worktree: Suppress bare .git events explained by filtered sibling events (#61636)
Fixes an infinite git-rescan loop on Windows introduced by #59876. On Windows, creating or deleting a file directly inside .git updates the directory's last-write time, so ReadDirectoryChangesW reports a bare .git Changed event alongside the file's own event. Since #59876, bare .git events schedule a git rescan (to cope with coalesced FSEvents on macOS), so every filtered-out lock-file event still triggered a rescan through its paired bare event. Because a rescan's own `git diff --numstat HEAD` can take .git/index.lock (even under --no-optional-locks, e.g. in jj-colocated repos whose index never refreshes clean), each rescan re-triggered the next one, spawning ~9 git processes per cycle, ~4 cycles/sec, indefinitely. Bare .git events are now deferred while processing an event batch and only trigger a rescan when the batch contains no filtered event for the same git dir that explains the directory change. A standalone bare .git event (the macOS coalescing case #59876 addressed) still triggers a rescan, as does any batch containing a meaningful .git change. Release Notes: - N/A or Added/Fixed/Improved ...
This commit is contained in:
1 parent
73be80748b
commit
fb813f70d6
2 files changed
+158
-9
No files matched your search
@@ -4773,6 +4773,18 @@ impl BackgroundScanner {
|
||||
let snapshot = &self.state.lock().await.snapshot;
|
||||
|
||||
let mut ranges_to_drop = SmallVec::<[Range<usize>; 4]>::new();
|
||||
// On Windows, creating or deleting a file directly inside `.git`
|
||||
// updates the directory's last-write time, so the watcher reports a
|
||||
// bare `.git` Changed event alongside the file's own event. When the
|
||||
// file event is filtered out (e.g. git's transient `index.lock`), the
|
||||
// bare event carries no information either, so acting on it would
|
||||
// turn every ignored lock file into a git rescan. Since a rescan's
|
||||
// own `git diff` can take `index.lock`, that feeds back into an
|
||||
// infinite loop of rescans. So bare `.git` events are deferred here
|
||||
// and only rescanned when nothing in the batch explains them; a
|
||||
// standalone bare event (macOS coalescing) still triggers a rescan.
|
||||
let mut bare_dot_git_abs_paths = Vec::new();
|
||||
let mut dot_git_dirs_with_ignored_events = Vec::new();
|
||||
|
||||
for (ix, event) in events.iter().enumerate() {
|
||||
let abs_path = SanitizedPath::new(&event.path);
|
||||
@@ -4816,6 +4828,21 @@ impl BackgroundScanner {
|
||||
log::debug!(
|
||||
"ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories"
|
||||
);
|
||||
if !dot_git_dirs_with_ignored_events.contains(&dot_git_abs_path) {
|
||||
dot_git_dirs_with_ignored_events.push(dot_git_abs_path);
|
||||
}
|
||||
skip_ix(&mut ranges_to_drop, ix);
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_dot_git {
|
||||
log::debug!(
|
||||
"ignoring event {abs_path:?} for .git directory itself (kind: {:?})",
|
||||
event.kind
|
||||
);
|
||||
if !bare_dot_git_abs_paths.contains(&dot_git_abs_path) {
|
||||
bare_dot_git_abs_paths.push(dot_git_abs_path);
|
||||
}
|
||||
skip_ix(&mut ranges_to_drop, ix);
|
||||
continue;
|
||||
}
|
||||
@@ -4827,15 +4854,6 @@ impl BackgroundScanner {
|
||||
dot_git_abs_paths.push(dot_git_abs_path);
|
||||
}
|
||||
|
||||
if is_dot_git {
|
||||
log::debug!(
|
||||
"ignoring event {abs_path:?} for .git directory itself (kind: {:?})",
|
||||
event.kind
|
||||
);
|
||||
skip_ix(&mut ranges_to_drop, ix);
|
||||
continue;
|
||||
}
|
||||
|
||||
// New directories can appear under the `refs` tree at any time, e.g. when a
|
||||
// remote is added or a branch name contains slashes. On platforms where the
|
||||
// native watcher is non-recursive they need their own watches, or subsequent
|
||||
@@ -4894,6 +4912,19 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
for dot_git_abs_path in bare_dot_git_abs_paths {
|
||||
if dot_git_dirs_with_ignored_events.contains(&dot_git_abs_path) {
|
||||
log::debug!(
|
||||
"not reloading git repo at {dot_git_abs_path:?}: the bare .git event is explained by filtered events in the same batch"
|
||||
);
|
||||
} else if !dot_git_abs_paths.contains(&dot_git_abs_path) {
|
||||
log::debug!(
|
||||
"detected update within git repo at {dot_git_abs_path:?}: bare .git directory event"
|
||||
);
|
||||
dot_git_abs_paths.push(dot_git_abs_path);
|
||||
}
|
||||
}
|
||||
|
||||
for range_to_drop in ranges_to_drop.into_iter().rev() {
|
||||
events.drain(range_to_drop);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user