worktree: Refresh all repositories sharing a changed git directory (#60664)

update_git_repositories mapped a changed .git path to a repository with
find_map, so when several repositories share a git directory - a main
checkout plus one of its linked worktrees in the same project worktree -
a ref update under the shared common dir only bumped git_dir_scan_id on
whichever repository iterated first, leaving the others stale until an
unrelated event happened to refresh them. Collect every matching
repository and bump each one.

---

Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth authored and GitHub committed 2026-07-09 12:41:50 +00:00
1 parent 7de1701b71
commit be9f38a892
2 files changed
+127 -46

No files matched your search

+47 -46
View File
@@ -5762,56 +5762,57 @@ impl BackgroundScanner {
let scan_id = state.snapshot.scan_id;
let mut affected_repo_roots = Vec::new();
for dot_git_dir in dot_git_paths {
let existing_repository_entry =
state
.snapshot
.git_repositories
.iter()
.find_map(|(_, repo)| {
let dot_git_dir = SanitizedPath::new(&dot_git_dir);
if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir
|| SanitizedPath::new(repo.repository_dir_abs_path.as_ref())
== dot_git_dir
|| SanitizedPath::new(repo.dot_git_abs_path.as_ref()) == dot_git_dir
{
Some(repo.clone())
} else {
None
}
});
// Several repositories can share a git directory: a linked worktree's
// commondir is the main checkout's `.git`, so a ref update there must
// refresh every repository that reads from it.
let existing_work_directory_ids = state
.snapshot
.git_repositories
.iter()
.filter_map(|(&work_directory_id, repo)| {
let dot_git_dir = SanitizedPath::new(&dot_git_dir);
if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir
|| SanitizedPath::new(repo.repository_dir_abs_path.as_ref()) == dot_git_dir
|| SanitizedPath::new(repo.dot_git_abs_path.as_ref()) == dot_git_dir
{
Some(work_directory_id)
} else {
None
}
})
.collect::<Vec<_>>();
match existing_repository_entry {
None => {
let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else {
// A `.git` path outside the worktree root is not
// ours to register. This happens legitimately when
// `.git` is a gitfile pointing outside the worktree
// (linked worktrees and submodules), and also when
// a rescan of a linked worktree's commondir arrives
// after the worktree's repository has already been
// unregistered.
continue;
};
affected_repo_roots.push(dot_git_dir.parent().unwrap().into());
if existing_work_directory_ids.is_empty() {
let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else {
// A `.git` path outside the worktree root is not
// ours to register. This happens legitimately when
// `.git` is a gitfile pointing outside the worktree
// (linked worktrees and submodules), and also when
// a rescan of a linked worktree's commondir arrives
// after the worktree's repository has already been
// unregistered.
continue;
};
affected_repo_roots.push(dot_git_dir.parent().unwrap().into());
state
.insert_git_repository(
RelPath::new(relative, PathStyle::local())
.unwrap()
.into_arc(),
self.fs.as_ref(),
self.watcher.as_ref(),
)
.await;
} else {
for work_directory_id in existing_work_directory_ids {
state
.insert_git_repository(
RelPath::new(relative, PathStyle::local())
.unwrap()
.into_arc(),
self.fs.as_ref(),
self.watcher.as_ref(),
)
.await;
}
Some(local_repository) => {
state.snapshot.git_repositories.update(
&local_repository.work_directory_id,
|entry| {
.snapshot
.git_repositories
.update(&work_directory_id, |entry| {
entry.git_dir_scan_id = scan_id;
},
);
});
}
};
}
}
// Remove any git repositories whose .git entry no longer exists.
@@ -4199,6 +4199,86 @@ async fn test_linked_worktree_gitfile_event_preserves_repo(
});
}
#[gpui::test]
async fn test_shared_common_dir_event_updates_all_repositories(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
// A main checkout and one of its linked worktrees can both live inside the
// same project worktree, sharing a common git directory. An event in that
// common directory (e.g. a ref update) must refresh every repository that
// reads from it, not just the first match.
init_test(cx);
use git::repository::Worktree as GitWorktree;
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main_repo": {
".git": {},
"file.txt": "content",
},
}),
)
.await;
fs.add_linked_worktree_for_repo(
Path::new(path!("/project/main_repo/.git")),
false,
GitWorktree {
path: PathBuf::from(path!("/project/linked")),
ref_name: Some("refs/heads/feature".into()),
sha: "abc123".into(),
is_main: false,
is_bare: false,
},
)
.await;
let tree = Worktree::local(
path!("/project").as_ref(),
true,
fs.clone(),
Arc::default(),
true,
WorktreeId::from_proto(0),
&mut cx.to_async(),
)
.await
.unwrap();
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
cx.run_until_parked();
let mut events = cx.events(&tree);
fs.emit_fs_event(
path!("/project/main_repo/.git/refs/heads/main"),
Some(PathEventKind::Changed),
);
executor.run_until_parked();
let mut updated_work_dirs = Vec::new();
while let Ok(event) = events.try_recv() {
if let Event::UpdatedGitRepositories(updates) = event {
updated_work_dirs.extend(
updates
.iter()
.filter_map(|update| update.new_work_directory_abs_path.clone()),
);
}
}
updated_work_dirs.sort();
assert_eq!(
updated_work_dirs,
[
Arc::from(Path::new(path!("/project/linked"))),
Arc::from(Path::new(path!("/project/main_repo"))),
],
"a ref update in the shared common dir should refresh both repositories"
);
}
#[gpui::test]
async fn test_noisy_dot_git_events_do_not_emit_git_repo_update(
executor: BackgroundExecutor,