git: Avoid unnecessary git repo rescans when unrelated git files change (#59318)
# Objective
Zed triggers many git rescans whenever an outside git command modifies
files inside `.git` dir. This becomes especially problematic when
working on large repos or doing remote development on machines with slow
filesystems.
## Solution
Events for object writes, hook files, lock files, and the reflogs of
HEAD/branches/remote-tracking branches carry no git changes that Zed
cares about beyond what the ref or events already cover. So changes to
these files shouldn't trigger a full git rescan.
## Testing
I extended the existing unittests to add the additionally ignored
directories and lock files.
I also manually verified the changes by viewing the zed git debug logs
that get generated when running `git gc` on a freshly gc'ed repo.
Previously Zed triggered **7 worktree updates**, with these changes it
was reduced to **a single worktree update** which is due to
`.git/packed-refs` which we can't ignore.
**main:**
```
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:52+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:52+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:53+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:53+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:54+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:54+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:54+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:54+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:55+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:55+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:55+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:55+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-13T13:56:55+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-13T13:56:56+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-13T13:56:56+01:00 DEBUG [project::git_store] fetched remotes
2026-06-13T13:56:56+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-13T13:56:56+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-13T13:56:56+01:00 DEBUG [project::git_store] load merge details
2026-06-13T13:56:56+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }
```
**This PR:**
```
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] received worktree update for repositories: [UpdatedGitRepository { work_directory_id: ProjectEntryId(0), old_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), new_work_directory_abs_path: Some("/Users/lgeiger/code/zed"), dot_git_abs_path: Some("/Users/lgeiger/code/zed/.git"), repository_dir_abs_path: Some("/Users/lgeiger/code/zed/.git"), common_dir_abs_path: Some("/Users/lgeiger/code/zed/.git") }]
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] local worktree repos changed
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] run scheduled git status scan
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] starting compute snapshot
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] fetched branches, head commit, worktrees
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] fetched remotes
2026-06-15T01:19:51+01:00 DEBUG [git::repository] Checking for git status in [""]
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] fetched statuses, diff stats, stash entries
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] load merge details
2026-06-15T01:19:51+01:00 DEBUG [project::git_store] new merge details: MergeDetails { merge_heads_by_conflicted_path: {}, message: None }```
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- Reduced number of git operations when repository state changes outside of zed
This commit is contained in:
1 parent
2252cad9b9
commit
2408640e5f
3 files changed
+101
-22
No files matched your search
+12
-1
@@ -20,8 +20,19 @@ pub const DOT_GIT: &str = ".git";
|
||||
pub const GITIGNORE: &str = ".gitignore";
|
||||
pub const FSMONITOR_DAEMON: &str = "fsmonitor--daemon";
|
||||
pub const LFS_DIR: &str = "lfs";
|
||||
pub const OBJECTS_DIR: &str = "objects";
|
||||
pub const HOOKS_DIR: &str = "hooks";
|
||||
pub const LOGS_DIR: &str = "logs";
|
||||
pub const LOGS_REF_STASH: &str = "logs/refs/stash";
|
||||
pub const REBASE_MERGE_DIR: &str = "rebase-merge";
|
||||
pub const REBASE_APPLY_DIR: &str = "rebase-apply";
|
||||
pub const SEQUENCER_DIR: &str = "sequencer";
|
||||
pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
|
||||
pub const INDEX_LOCK: &str = "index.lock";
|
||||
pub const FETCH_HEAD: &str = "FETCH_HEAD";
|
||||
pub const ORIG_HEAD: &str = "ORIG_HEAD";
|
||||
pub const BISECT_LOG: &str = "BISECT_LOG";
|
||||
pub const GC_PID: &str = "gc.pid";
|
||||
pub const INFO_DIR: &str = "info";
|
||||
pub const REPO_EXCLUDE: &str = "info/exclude";
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -22,8 +22,9 @@ use futures::{
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::{
|
||||
COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, REPO_EXCLUDE,
|
||||
status::GitSummary,
|
||||
BISECT_LOG, COMMIT_MESSAGE, DOT_GIT, FETCH_HEAD, FSMONITOR_DAEMON, GC_PID, GITIGNORE,
|
||||
HOOKS_DIR, INFO_DIR, LFS_DIR, LOGS_DIR, LOGS_REF_STASH, OBJECTS_DIR, ORIG_HEAD,
|
||||
REBASE_APPLY_DIR, REBASE_MERGE_DIR, REPO_EXCLUDE, SEQUENCER_DIR, status::GitSummary,
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority,
|
||||
@@ -4444,8 +4445,17 @@ impl BackgroundScanner {
|
||||
//
|
||||
// Certain directories may have FS changes, but do not lead to git data changes that Zed cares about.
|
||||
// Ignore these, to avoid Zed unnecessarily rescanning git metadata.
|
||||
let skipped_file_names_in_dot_git = [COMMIT_MESSAGE, INDEX_LOCK];
|
||||
let skipped_dirs_in_dot_git = [FSMONITOR_DAEMON, LFS_DIR];
|
||||
let skipped_file_names_in_dot_git =
|
||||
[COMMIT_MESSAGE, FETCH_HEAD, ORIG_HEAD, BISECT_LOG, GC_PID];
|
||||
let skipped_dirs_in_dot_git = [
|
||||
FSMONITOR_DAEMON,
|
||||
LFS_DIR,
|
||||
OBJECTS_DIR,
|
||||
HOOKS_DIR,
|
||||
REBASE_MERGE_DIR,
|
||||
REBASE_APPLY_DIR,
|
||||
SEQUENCER_DIR,
|
||||
];
|
||||
|
||||
let mut dot_git_abs_paths = Vec::new();
|
||||
let mut work_dirs_needing_exclude_update = Vec::new();
|
||||
@@ -4478,9 +4488,18 @@ impl BackgroundScanner {
|
||||
path_in_git_dir
|
||||
.file_name()
|
||||
.is_some_and(|file_name| file_name == OsStr::new(skipped))
|
||||
}) || skipped_dirs_in_dot_git
|
||||
.iter()
|
||||
.any(|skipped_git_subdir| path_in_git_dir.starts_with(skipped_git_subdir));
|
||||
}) || (path_in_git_dir.starts_with(LOGS_DIR)
|
||||
&& path_in_git_dir != Path::new(LOGS_REF_STASH))
|
||||
|| (path_in_git_dir.starts_with(INFO_DIR)
|
||||
&& path_in_git_dir != Path::new(REPO_EXCLUDE))
|
||||
|| skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| {
|
||||
path_in_git_dir.starts_with(skipped_git_subdir)
|
||||
})
|
||||
|| path_in_git_dir.extension().is_some_and(|ext| ext == "lock")
|
||||
|| (path_in_git_dir.components().count() == 1
|
||||
&& path_in_git_dir
|
||||
.extension()
|
||||
.is_some_and(|ext| ext == "new" || ext == "tmp"));
|
||||
let is_dot_git = path_in_git_dir == Path::new("")
|
||||
&& matches!(event.kind, Some(PathEventKind::Changed))
|
||||
&& self.fs.is_dir(&dot_git_abs_path).await;
|
||||
|
||||
@@ -4108,14 +4108,16 @@ async fn test_linked_worktree_gitfile_event_preserves_repo(
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_linked_worktree_index_lock_event_does_not_emit_git_repo_update(
|
||||
async fn test_noisy_dot_git_events_do_not_emit_git_repo_update(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
// Regression test: in a linked worktree, git operations like `git status`
|
||||
// can touch the worktree-specific `index.lock` under the main repo's
|
||||
// `.git/worktrees/<name>/`. We intend to ignore those events so they do not
|
||||
// spuriously emit `UpdatedGitRepositories`.
|
||||
// Events for object database writes, hook files, lock files, and the
|
||||
// reflogs of HEAD/branches/remote-tracking branches carry no git state
|
||||
// changes that Zed cares about beyond what the accompanying ref or index
|
||||
// events already convey, so they must not trigger a git metadata rescan.
|
||||
// The stash reflog and ref updates themselves must still trigger one.
|
||||
//
|
||||
init_test(cx);
|
||||
|
||||
use git::repository::Worktree as GitWorktree;
|
||||
@@ -4177,17 +4179,64 @@ async fn test_linked_worktree_index_lock_event_does_not_emit_git_repo_update(
|
||||
}
|
||||
});
|
||||
|
||||
fs.emit_fs_event(
|
||||
let skipped_paths = [
|
||||
// Standard common git dir skipped paths
|
||||
path!("/main_repo/.git/objects/aa/bbccddee"),
|
||||
path!("/main_repo/.git/objects/pack/pack-1234.pack"),
|
||||
path!("/main_repo/.git/hooks/pre-commit"),
|
||||
path!("/main_repo/.git/logs/HEAD"),
|
||||
path!("/main_repo/.git/logs/refs/heads/main"),
|
||||
path!("/main_repo/.git/logs/refs/remotes/origin/main"),
|
||||
path!("/main_repo/.git/logs/refs/tags/v1.0"),
|
||||
path!("/main_repo/.git/rebase-merge/done"),
|
||||
path!("/main_repo/.git/rebase-apply/onto"),
|
||||
path!("/main_repo/.git/sequencer/todo"),
|
||||
path!("/main_repo/.git/index.lock"),
|
||||
path!("/main_repo/.git/refs/heads/main.lock"),
|
||||
path!("/main_repo/.git/COMMIT_EDITMSG"),
|
||||
path!("/main_repo/.git/packed-refs.new"),
|
||||
path!("/main_repo/.git/config.new"),
|
||||
path!("/main_repo/.git/index.new"),
|
||||
path!("/main_repo/.git/index-abc123.tmp"),
|
||||
path!("/main_repo/.git/FETCH_HEAD"),
|
||||
path!("/main_repo/.git/ORIG_HEAD"),
|
||||
path!("/main_repo/.git/BISECT_LOG"),
|
||||
path!("/main_repo/.git/info/refs"),
|
||||
path!("/main_repo/.git/info/refs_lzOf51"),
|
||||
path!("/main_repo/.git/gc.pid"),
|
||||
// Linked-worktree specific skipped paths
|
||||
path!("/main_repo/.git/worktrees/feature/index.lock"),
|
||||
Some(PathEventKind::Changed),
|
||||
);
|
||||
cx.run_until_parked();
|
||||
];
|
||||
for path in skipped_paths {
|
||||
fs.emit_fs_event(path, Some(PathEventKind::Changed));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
repo_update_count.get(),
|
||||
0,
|
||||
"event for {path} should not emit UpdatedGitRepositories"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
repo_update_count.get(),
|
||||
0,
|
||||
"linked-worktree index.lock events should not emit UpdatedGitRepositories"
|
||||
);
|
||||
let rescan_paths = [
|
||||
// Standard common git dir rescan paths
|
||||
path!("/main_repo/.git/logs/refs/stash"),
|
||||
path!("/main_repo/.git/refs/heads/main"),
|
||||
path!("/main_repo/.git/info/exclude"),
|
||||
path!("/main_repo/.git/refs/heads/branch.new"),
|
||||
path!("/main_repo/.git/refs/heads/branch.tmp"),
|
||||
// Linked-worktree worktree-specific rescan paths
|
||||
path!("/main_repo/.git/worktrees/feature/index"),
|
||||
path!("/main_repo/.git/worktrees/feature/HEAD"),
|
||||
];
|
||||
for path in rescan_paths {
|
||||
let count_before = repo_update_count.get();
|
||||
fs.emit_fs_event(path, Some(PathEventKind::Changed));
|
||||
cx.run_until_parked();
|
||||
assert!(
|
||||
repo_update_count.get() > count_before,
|
||||
"event for {path} should emit UpdatedGitRepositories"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
Reference in new issue
Block a user