Move repository state RPC handlers to the GitStore (#27391)

This is another in the series of PRs to make the GitStore own all
repository state and enable better concurrency control for git
repository scans.

After this PR, the `RepositoryEntry`s stored in worktree snapshots are
used only as a staging ground for local GitStores to pull from after
git-related events; non-local worktrees don't store them at all,
although this is not reflected in the types. GitTraversal and other
places that need information about repositories get it from the
GitStore. The GitStore also takes over handling of the new
UpdateRepository and RemoveRepository messages. However, repositories
are still discovered and scanned on a per-worktree basis, and we're
still identifying them by the (worktree-specific) project entry ID of
their working directory.

- [x] Remove WorkDirectory from RepositoryEntry
- [x] Remove worktree IDs from repository-related RPC messages
- [x] Handle UpdateRepository and RemoveRepository RPCs from the
GitStore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Cole Miller 2025-03-26 18:23:44 -04:00 committed by GitHub
parent 1e8b50f471
commit 6924720b35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1387 additions and 1399 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,28 @@
use collections::HashMap;
use git::status::GitSummary;
use std::{ops::Deref, path::Path};
use sum_tree::Cursor;
use text::Bias;
use worktree::{Entry, PathProgress, PathTarget, RepositoryEntry, StatusEntry, Traversal};
use worktree::{
Entry, PathProgress, PathTarget, ProjectEntryId, RepositoryEntry, StatusEntry, Traversal,
};
/// Walks the worktree entries and their associated git statuses.
pub struct GitTraversal<'a> {
traversal: Traversal<'a>,
current_entry_summary: Option<GitSummary>,
repo_location: Option<(
&'a RepositoryEntry,
Cursor<'a, StatusEntry, PathProgress<'a>>,
)>,
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
repo_location: Option<(ProjectEntryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
}
impl<'a> GitTraversal<'a> {
pub fn new(traversal: Traversal<'a>) -> GitTraversal<'a> {
pub fn new(
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
traversal: Traversal<'a>,
) -> GitTraversal<'a> {
let mut this = GitTraversal {
traversal,
repo_snapshots,
current_entry_summary: None,
repo_location: None,
};
@ -32,7 +37,20 @@ impl<'a> GitTraversal<'a> {
return;
};
let Some(repo) = self.traversal.snapshot().repository_for_path(&entry.path) else {
let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
self.repo_location = None;
return;
};
let Some((repo, repo_path)) = self
.repo_snapshots
.values()
.filter_map(|repo_snapshot| {
let relative_path = repo_snapshot.relativize_abs_path(&abs_path)?;
Some((repo_snapshot, relative_path))
})
.max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
else {
self.repo_location = None;
return;
};
@ -42,18 +60,19 @@ impl<'a> GitTraversal<'a> {
|| self
.repo_location
.as_ref()
.map(|(prev_repo, _)| &prev_repo.work_directory)
!= Some(&repo.work_directory)
.map(|(prev_repo_id, _)| *prev_repo_id)
!= Some(repo.work_directory_id())
{
self.repo_location = Some((repo, repo.statuses_by_path.cursor::<PathProgress>(&())));
self.repo_location = Some((
repo.work_directory_id(),
repo.statuses_by_path.cursor::<PathProgress>(&()),
));
}
let Some((repo, statuses)) = &mut self.repo_location else {
let Some((_, statuses)) = &mut self.repo_location else {
return;
};
let repo_path = repo.relativize(&entry.path).unwrap();
if entry.is_dir() {
let mut statuses = statuses.clone();
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
@ -128,9 +147,15 @@ pub struct ChildEntriesGitIter<'a> {
}
impl<'a> ChildEntriesGitIter<'a> {
pub fn new(snapshot: &'a worktree::Snapshot, parent_path: &'a Path) -> Self {
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, true, true, parent_path));
pub fn new(
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
worktree_snapshot: &'a worktree::Snapshot,
parent_path: &'a Path,
) -> Self {
let mut traversal = GitTraversal::new(
repo_snapshots,
worktree_snapshot.traverse_from_path(true, true, true, parent_path),
);
traversal.advance();
ChildEntriesGitIter {
parent_path,
@ -215,6 +240,8 @@ impl AsRef<Entry> for GitEntry {
mod tests {
use std::time::Duration;
use crate::Project;
use super::*;
use fs::FakeFs;
use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode};
@ -222,7 +249,7 @@ mod tests {
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use util::path;
use worktree::{Worktree, WorktreeSettings};
use worktree::WorktreeSettings;
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
@ -282,44 +309,35 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, false, true, Path::new("x")));
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
assert_eq!(entry.git_summary, MODIFIED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
assert_eq!(entry.git_summary, ADDED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
let entry = traversal.next().unwrap();
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
assert_eq!(entry.git_summary, ADDED);
let traversal = GitTraversal::new(
&repo_snapshots,
worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
);
let entries = traversal
.map(|entry| (entry.path.clone(), entry.git_summary))
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
entries,
[
(Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/x2.txt").into(), MODIFIED),
(Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
(Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
(Path::new("x/z.txt").into(), ADDED),
(Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
(Path::new("z/z2.txt").into(), ADDED),
]
)
}
#[gpui::test]
@ -366,23 +384,20 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
// Sanity check the propagation for x/y and z
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x/y"), GitSummary::CONFLICT),
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
@ -390,7 +405,8 @@ mod tests {
],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), ADDED),
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
@ -400,7 +416,8 @@ mod tests {
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/y"), GitSummary::CONFLICT),
@ -410,7 +427,8 @@ mod tests {
// Sanity check everything around it
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), MODIFIED + ADDED),
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
@ -424,7 +442,8 @@ mod tests {
// Test the other fundamental case, transitioning from git repository to non-git repository
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
@ -434,7 +453,8 @@ mod tests {
// And all together now
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::UNCHANGED),
(Path::new("x"), MODIFIED + ADDED),
@ -490,21 +510,19 @@ mod tests {
],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
(Path::new("g"), GitSummary::CONFLICT),
@ -513,7 +531,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
(Path::new("a"), ADDED + MODIFIED),
@ -530,7 +549,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b"), ADDED),
(Path::new("a/b/c1.txt"), ADDED),
@ -545,7 +565,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("a/b/c1.txt"), ADDED),
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
@ -598,26 +619,25 @@ mod tests {
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
);
let tree = Worktree::local(
Path::new(path!("/root")),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
@ -626,7 +646,8 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("z"), MODIFIED),
(Path::new("z/z2.txt"), MODIFIED),
@ -634,12 +655,14 @@ mod tests {
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
);
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new("x"), ADDED),
(Path::new("x/x1.txt"), ADDED),
@ -689,18 +712,11 @@ mod tests {
);
cx.run_until_parked();
let tree = Worktree::local(
path!("/root").as_ref(),
true,
fs.clone(),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
cx.executor().run_until_parked();
let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
(
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
@ -713,7 +729,8 @@ mod tests {
fs.touch_path(path!("/root")).await;
cx.executor().run_until_parked();
let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
(
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
@ -734,10 +751,16 @@ mod tests {
cx.executor().run_until_parked();
cx.executor().advance_clock(Duration::from_secs(1));
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
(
project.git_store().read(cx).repo_snapshots(cx),
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
)
});
check_git_statuses(
&snapshot,
&repo_snapshots,
&worktree_snapshot,
&[
(Path::new(""), MODIFIED),
(Path::new("a.txt"), GitSummary::UNCHANGED),
@ -748,11 +771,14 @@ mod tests {
#[track_caller]
fn check_git_statuses(
snapshot: &worktree::Snapshot,
repo_snapshots: &HashMap<ProjectEntryId, RepositoryEntry>,
worktree_snapshot: &worktree::Snapshot,
expected_statuses: &[(&Path, GitSummary)],
) {
let mut traversal =
GitTraversal::new(snapshot.traverse_from_path(true, true, false, "".as_ref()));
let mut traversal = GitTraversal::new(
repo_snapshots,
worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
);
let found_statuses = expected_statuses
.iter()
.map(|&(path, _)| {
@ -762,6 +788,6 @@ mod tests {
(path, git_entry.git_summary)
})
.collect::<Vec<_>>();
assert_eq!(found_statuses, expected_statuses);
pretty_assertions::assert_eq!(found_statuses, expected_statuses);
}
}

View file

@ -24,7 +24,7 @@ mod direnv;
mod environment;
use buffer_diff::BufferDiff;
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
use git_store::Repository;
use git_store::{GitEvent, Repository};
pub mod search_history;
mod yarn;
@ -270,7 +270,6 @@ pub enum Event {
WorktreeOrderChanged,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId,
},
@ -300,6 +299,8 @@ pub enum Event {
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
GitStateUpdated,
ActiveRepositoryChanged,
}
pub enum DebugAdapterClientState {
@ -793,8 +794,6 @@ impl Project {
client.add_entity_message_handler(Self::handle_unshare_project);
client.add_entity_request_handler(Self::handle_update_buffer);
client.add_entity_message_handler(Self::handle_update_worktree);
client.add_entity_message_handler(Self::handle_update_repository);
client.add_entity_message_handler(Self::handle_remove_repository);
client.add_entity_request_handler(Self::handle_synchronize_buffers);
client.add_entity_request_handler(Self::handle_search_candidate_buffers);
@ -922,6 +921,7 @@ impl Project {
cx,
)
});
cx.subscribe(&git_store, Self::on_git_store_event).detach();
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@ -1136,8 +1136,6 @@ impl Project {
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
ssh_proto.add_entity_message_handler(Self::handle_update_repository);
ssh_proto.add_entity_message_handler(Self::handle_remove_repository);
ssh_proto.add_entity_message_handler(Self::handle_update_project);
ssh_proto.add_entity_message_handler(Self::handle_toast);
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
@ -2040,6 +2038,11 @@ impl Project {
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.send_project_updates(cx);
});
if let Some(remote_id) = self.remote_id() {
self.git_store.update(cx, |git_store, cx| {
git_store.shared(remote_id, self.client.clone().into(), cx)
});
}
cx.emit(Event::Reshared);
Ok(())
}
@ -2707,6 +2710,19 @@ impl Project {
}
}
fn on_git_store_event(
&mut self,
_: Entity<GitStore>,
event: &GitEvent,
cx: &mut Context<Self>,
) {
match event {
GitEvent::GitStateUpdated => cx.emit(Event::GitStateUpdated),
GitEvent::ActiveRepositoryChanged => cx.emit(Event::ActiveRepositoryChanged),
GitEvent::FileSystemUpdated | GitEvent::IndexWriteError(_) => {}
}
}
fn on_ssh_event(
&mut self,
_: Entity<SshRemoteClient>,
@ -2792,12 +2808,11 @@ impl Project {
.report_discovered_project_events(*worktree_id, changes);
cx.emit(Event::WorktreeUpdatedEntries(*worktree_id, changes.clone()))
}
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id) => {
cx.emit(Event::WorktreeUpdatedGitRepositories(*worktree_id))
}
WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, id) => {
cx.emit(Event::DeletedEntry(*worktree_id, *id))
}
// Listen to the GitStore instead.
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_, _) => {}
}
}
@ -4309,43 +4324,7 @@ impl Project {
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
});
}
Ok(())
})?
}
async fn handle_update_repository(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateRepository>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if let Some((worktree, _relative_path)) =
this.find_worktree(envelope.payload.abs_path.as_ref(), cx)
{
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
});
}
Ok(())
})?
}
async fn handle_remove_repository(
this: Entity<Self>,
envelope: TypedEnvelope<proto::RemoveRepository>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if let Some(worktree) =
this.worktree_for_entry(ProjectEntryId::from_proto(envelope.payload.id), cx)
{
worktree.update(cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope.payload.into());
worktree.update_from_remote(envelope.payload);
});
}
Ok(())

View file

@ -6,7 +6,8 @@ use buffer_diff::{
};
use fs::FakeFs;
use futures::{future, StreamExt};
use gpui::{App, SemanticVersion, UpdateGlobal};
use git::repository::RepoPath;
use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
use http_client::Url;
use language::{
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
@ -34,6 +35,7 @@ use util::{
test::{marked_text_offsets, TempTree},
uri, TryFutureExt as _,
};
use worktree::WorktreeModelHandle as _;
#[gpui::test]
async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
@ -6769,6 +6771,158 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_repository_and_path_for_project_path(
background_executor: BackgroundExecutor,
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(background_executor);
fs.insert_tree(
path!("/root"),
json!({
"c.txt": "",
"dir1": {
".git": {},
"deps": {
"dep1": {
".git": {},
"src": {
"a.txt": ""
}
}
},
"src": {
"b.txt": ""
}
},
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let git_store = project.git_store().read(cx);
let pairs = [
("c.txt", None),
("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
(
"dir1/deps/dep1/src/a.txt",
Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
),
];
let expected = pairs
.iter()
.map(|(path, result)| {
(
path,
result.map(|(repo, repo_path)| {
(Path::new(repo).to_owned(), RepoPath::from(repo_path))
}),
)
})
.collect::<Vec<_>>();
let actual = pairs
.iter()
.map(|(path, _)| {
let project_path = (tree_id, Path::new(path)).into();
let result = maybe!({
let (repo, repo_path) =
git_store.repository_and_path_for_project_path(&project_path, cx)?;
Some((
repo.read(cx)
.repository_entry
.work_directory_abs_path
.clone(),
repo_path,
))
});
(path, result)
})
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(expected, actual);
});
fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
.await
.unwrap();
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let git_store = project.git_store().read(cx);
assert_eq!(
git_store.repository_and_path_for_project_path(
&(tree_id, Path::new("dir1/src/b.txt")).into(),
cx
),
None
);
});
}
#[gpui::test]
async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/root"),
json!({
"home": {
".git": {},
"project": {
"a.txt": "A"
},
},
}),
)
.await;
fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let containing = project
.git_store()
.read(cx)
.repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
assert!(containing.is_none());
});
let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let tree_id = tree.read_with(cx, |tree, _| tree.id());
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
project.read_with(cx, |project, cx| {
let containing = project
.git_store()
.read(cx)
.repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
assert_eq!(
containing
.unwrap()
.0
.read(cx)
.repository_entry
.work_directory_abs_path,
Path::new(path!("/root/home"))
);
});
}
async fn search(
project: &Entity<Project>,
query: SearchQuery,

View file

@ -26,7 +26,10 @@ use smol::{
};
use text::ReplicaId;
use util::{paths::SanitizedPath, ResultExt};
use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
use worktree::{
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
WorktreeSettings,
};
use crate::{search::SearchQuery, ProjectPath};
@ -66,7 +69,7 @@ pub enum WorktreeStoreEvent {
WorktreeOrderChanged,
WorktreeUpdateSent(Entity<Worktree>),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
WorktreeUpdatedGitRepositories(WorktreeId, UpdatedGitRepositoriesSet),
WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
}
@ -156,6 +159,11 @@ impl WorktreeStore {
None
}
pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
worktree.read(cx).absolutize(&project_path.path).ok()
}
pub fn find_or_create_worktree(
&mut self,
abs_path: impl AsRef<Path>,
@ -367,9 +375,10 @@ impl WorktreeStore {
changes.clone(),
));
}
worktree::Event::UpdatedGitRepositories(_) => {
worktree::Event::UpdatedGitRepositories(set) => {
cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
worktree_id,
set.clone(),
));
}
worktree::Event::DeletedEntry(id) => {
@ -561,44 +570,12 @@ impl WorktreeStore {
let client = client.clone();
async move {
if client.is_via_collab() {
match update {
proto::WorktreeRelatedMessage::UpdateWorktree(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
proto::WorktreeRelatedMessage::UpdateRepository(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
proto::WorktreeRelatedMessage::RemoveRepository(
update,
) => {
client
.request(update)
.map(|result| result.log_err().is_some())
.await
}
}
client
.request(update)
.map(|result| result.log_err().is_some())
.await
} else {
match update {
proto::WorktreeRelatedMessage::UpdateWorktree(
update,
) => client.send(update).log_err().is_some(),
proto::WorktreeRelatedMessage::UpdateRepository(
update,
) => client.send(update).log_err().is_some(),
proto::WorktreeRelatedMessage::RemoveRepository(
update,
) => client.send(update).log_err().is_some(),
}
client.send(update).log_err().is_some()
}
}
}