git: Track worktree references to resolve stale repository state (#41592)

Closes #35997
Closes #38018
Closes #41516

Release Notes:
- Fixes stale git repositories persisting after removal
This commit is contained in:
Mayank Verma
2025-11-09 21:24:20 -05:00
committed by GitHub
parent 0bcf607a28
commit c60d31a726
2 changed files with 170 additions and 2 deletions
+57 -2
View File
@@ -55,9 +55,10 @@ use rpc::{
proto::{self, git_reset, split_repository_update},
};
use serde::Deserialize;
use settings::WorktreeId;
use std::{
cmp::Ordering,
collections::{BTreeSet, VecDeque},
collections::{BTreeSet, HashSet, VecDeque},
future::Future,
mem,
ops::Range,
@@ -89,6 +90,7 @@ pub struct GitStore {
buffer_store: Entity<BufferStore>,
worktree_store: Entity<WorktreeStore>,
repositories: HashMap<RepositoryId, Entity<Repository>>,
worktree_ids: HashMap<RepositoryId, HashSet<WorktreeId>>,
active_repo_id: Option<RepositoryId>,
#[allow(clippy::type_complexity)]
loading_diffs:
@@ -409,6 +411,7 @@ impl GitStore {
buffer_store,
worktree_store,
repositories: HashMap::default(),
worktree_ids: HashMap::default(),
active_repo_id: None,
_subscriptions,
loading_diffs: HashMap::default(),
@@ -1167,6 +1170,7 @@ impl GitStore {
return;
}
self.update_repositories_from_worktree(
*worktree_id,
project_environment.clone(),
next_repository_id.clone(),
downstream
@@ -1178,6 +1182,45 @@ impl GitStore {
);
self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
}
WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => {
let repos_without_worktree: Vec<RepositoryId> = self
.worktree_ids
.iter_mut()
.filter_map(|(repo_id, worktree_ids)| {
worktree_ids.remove(worktree_id);
if worktree_ids.is_empty() {
Some(*repo_id)
} else {
None
}
})
.collect();
let is_active_repo_removed = repos_without_worktree
.iter()
.any(|repo_id| self.active_repo_id == Some(*repo_id));
for repo_id in repos_without_worktree {
self.repositories.remove(&repo_id);
self.worktree_ids.remove(&repo_id);
if let Some(updates_tx) =
downstream.as_ref().map(|downstream| &downstream.updates_tx)
{
updates_tx
.unbounded_send(DownstreamUpdate::RemoveRepository(repo_id))
.ok();
}
}
if is_active_repo_removed {
if let Some((&repo_id, _)) = self.repositories.iter().next() {
self.active_repo_id = Some(repo_id);
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id)));
} else {
self.active_repo_id = None;
cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
}
}
}
_ => {}
}
}
@@ -1228,6 +1271,7 @@ impl GitStore {
/// Update our list of repositories and schedule git scans in response to a notification from a worktree,
fn update_repositories_from_worktree(
&mut self,
worktree_id: WorktreeId,
project_environment: Entity<ProjectEnvironment>,
next_repository_id: Arc<AtomicU64>,
updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
@@ -1245,15 +1289,25 @@ impl GitStore {
|| Some(&existing_work_directory_abs_path)
== update.new_work_directory_abs_path.as_ref()
}) {
let repo_id = *id;
if let Some(new_work_directory_abs_path) =
update.new_work_directory_abs_path.clone()
{
self.worktree_ids
.entry(repo_id)
.or_insert_with(HashSet::new)
.insert(worktree_id);
existing.update(cx, |existing, cx| {
existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
existing.schedule_scan(updates_tx.clone(), cx);
});
} else {
removed_ids.push(*id);
if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) {
worktree_ids.remove(&worktree_id);
if worktree_ids.is_empty() {
removed_ids.push(repo_id);
}
}
}
} else if let UpdatedGitRepository {
new_work_directory_abs_path: Some(work_directory_abs_path),
@@ -1291,6 +1345,7 @@ impl GitStore {
self._subscriptions
.push(cx.subscribe(&repo, Self::on_jobs_updated));
self.repositories.insert(id, repo);
self.worktree_ids.insert(id, HashSet::from([worktree_id]));
cx.emit(GitStoreEvent::RepositoryAdded);
self.active_repo_id.get_or_insert_with(|| {
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
+113
View File
@@ -10477,3 +10477,116 @@ async fn test_find_project_path_abs(
);
});
}
#[gpui::test]
async fn test_git_worktree_remove(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"a": {
".git": {},
"src": {
"main.rs": "fn main() {}",
}
},
"b": {
".git": {},
"src": {
"main.rs": "fn main() {}",
},
"script": {
"run.sh": "#!/bin/bash"
}
}
}),
)
.await;
let project = Project::test(
fs.clone(),
[
path!("/root/a").as_ref(),
path!("/root/b/script").as_ref(),
path!("/root/b").as_ref(),
],
cx,
)
.await;
let scan_complete = project.update(cx, |project, cx| project.git_scans_complete(cx));
scan_complete.await;
let worktrees = project.update(cx, |project, cx| project.worktrees(cx).collect::<Vec<_>>());
assert_eq!(worktrees.len(), 3);
let worktree_id_by_abs_path = worktrees
.into_iter()
.map(|worktree| worktree.read_with(cx, |w, _| (w.abs_path(), w.id())))
.collect::<HashMap<_, _>>();
let worktree_id = worktree_id_by_abs_path
.get(Path::new(path!("/root/b/script")))
.unwrap();
let repos = project.update(cx, |p, cx| p.git_store().read(cx).repositories().clone());
assert_eq!(repos.len(), 2);
project.update(cx, |project, cx| {
project.remove_worktree(*worktree_id, cx);
});
cx.run_until_parked();
let mut repo_paths = project
.update(cx, |p, cx| p.git_store().read(cx).repositories().clone())
.values()
.map(|repo| repo.read_with(cx, |r, _| r.work_directory_abs_path.clone()))
.collect::<Vec<_>>();
repo_paths.sort();
pretty_assertions::assert_eq!(
repo_paths,
[
Path::new(path!("/root/a")).into(),
Path::new(path!("/root/b")).into(),
]
);
let active_repo_path = project
.read_with(cx, |p, cx| {
p.active_repository(cx)
.map(|r| r.read(cx).work_directory_abs_path.clone())
})
.unwrap();
assert_eq!(active_repo_path.as_ref(), Path::new(path!("/root/a")));
let worktree_id = worktree_id_by_abs_path
.get(Path::new(path!("/root/a")))
.unwrap();
project.update(cx, |project, cx| {
project.remove_worktree(*worktree_id, cx);
});
cx.run_until_parked();
let active_repo_path = project
.read_with(cx, |p, cx| {
p.active_repository(cx)
.map(|r| r.read(cx).work_directory_abs_path.clone())
})
.unwrap();
assert_eq!(active_repo_path.as_ref(), Path::new(path!("/root/b")));
let worktree_id = worktree_id_by_abs_path
.get(Path::new(path!("/root/b")))
.unwrap();
project.update(cx, |project, cx| {
project.remove_worktree(*worktree_id, cx);
});
cx.run_until_parked();
let active_repo_path = project.read_with(cx, |p, cx| {
p.active_repository(cx)
.map(|r| r.read(cx).work_directory_abs_path.clone())
});
assert!(active_repo_path.is_none());
}