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
13 changes: 12 additions & 1 deletion crates/git/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment on lines +27 to +29

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These specific ignores have been found by Gemini, but I think that's correct since git only writes intermediate state to these files

pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
pub const INDEX_LOCK: &str = "index.lock";

@lgeiger lgeiger Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now ignore all *.lock files

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!(
Expand Down
33 changes: 26 additions & 7 deletions crates/worktree/src/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4420,8 +4421,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();
Expand Down Expand Up @@ -4454,9 +4464,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;
Expand Down
77 changes: 63 additions & 14 deletions crates/worktree/tests/integration/worktree_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down
Loading