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:
1 parent
7de1701b71
commit
be9f38a892
2 files changed
+127
-46
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user