Finish removing git repository state and scanning logic from worktrees (#27568)

Cole Miller , Max Brunsfeld , and Conrad created

This PR completes the process of moving git repository state storage and
scanning logic from the worktree crate to `project::git_store`.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>

Change summary

Cargo.lock                                                    |   2 
crates/activity_indicator/src/activity_indicator.rs           |  11 
crates/assistant2/src/thread.rs                               |  71 
crates/call/src/call_impl/room.rs                             |   2 
crates/collab/src/db/queries/projects.rs                      |   9 
crates/collab/src/db/queries/rooms.rs                         |   3 
crates/collab/src/tests/integration_tests.rs                  |  11 
crates/collab/src/tests/random_project_collaboration_tests.rs |  17 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |   6 
crates/editor/src/git/blame.rs                                |  25 
crates/fs/src/fake_git_repo.rs                                |  31 
crates/git/src/repository.rs                                  | 163 
crates/git_ui/src/branch_picker.rs                            |   4 
crates/git_ui/src/commit_modal.rs                             |   2 
crates/git_ui/src/git_panel.rs                                |  55 
crates/git_ui/src/project_diff.rs                             |  41 
crates/multi_buffer/src/multi_buffer.rs                       |   1 
crates/project/Cargo.toml                                     |   1 
crates/project/src/buffer_store.rs                            |  15 
crates/project/src/connection_manager.rs                      |   2 
crates/project/src/debugger/dap_store.rs                      |   4 
crates/project/src/environment.rs                             | 169 
crates/project/src/git_store.rs                               | 793 ++-
crates/project/src/git_store/git_traversal.rs                 |  27 
crates/project/src/lsp_store.rs                               |  11 
crates/project/src/project.rs                                 |  34 
crates/project/src/project_tests.rs                           | 983 ++++
crates/project/src/task_store.rs                              |   2 
crates/project/src/toolchain_store.rs                         |   6 
crates/project_panel/src/project_panel.rs                     |  20 
crates/proto/proto/zed.proto                                  |  41 
crates/proto/src/proto.rs                                     |  46 
crates/remote_server/src/remote_editing_tests.rs              |   9 
crates/sum_tree/src/tree_map.rs                               |   8 
crates/title_bar/src/title_bar.rs                             |   2 
crates/vim/src/normal/search.rs                               |   1 
crates/worktree/Cargo.toml                                    |   1 
crates/worktree/src/worktree.rs                               | 684 --
crates/worktree/src/worktree_tests.rs                         | 970 ----
39 files changed, 1,944 insertions(+), 2,339 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10591,6 +10591,7 @@ dependencies = [
  "fuzzy",
  "git",
  "git2",
+ "git_hosting_providers",
  "globset",
  "gpui",
  "http_client",
@@ -17217,7 +17218,6 @@ dependencies = [
  "fuzzy",
  "git",
  "git2",
- "git_hosting_providers",
  "gpui",
  "http_client",
  "ignore",

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -10,10 +10,10 @@ use gpui::{
 use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
 use project::{
     EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
-    ProjectEnvironmentEvent, WorktreeId,
+    ProjectEnvironmentEvent,
 };
 use smallvec::SmallVec;
-use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
+use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration};
 use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
 use util::truncate_and_trailoff;
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -218,13 +218,14 @@ impl ActivityIndicator {
     fn pending_environment_errors<'a>(
         &'a self,
         cx: &'a App,
-    ) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
+    ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
         self.project.read(cx).shell_environment_errors(cx)
     }
 
     fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
         // Show if any direnv calls failed
-        if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
+        if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
+            let abs_path = abs_path.clone();
             return Some(Content {
                 icon: Some(
                     Icon::new(IconName::Warning)
@@ -234,7 +235,7 @@ impl ActivityIndicator {
                 message: error.0.clone(),
                 on_click: Some(Arc::new(move |this, window, cx| {
                     this.project.update(cx, |project, cx| {
-                        project.remove_environment_error(worktree_id, cx);
+                        project.remove_environment_error(&abs_path, cx);
                     });
                     window.dispatch_action(Box::new(workspace::OpenLog), cx);
                 })),

crates/assistant2/src/thread.rs 🔗

@@ -11,7 +11,7 @@ use collections::{BTreeMap, HashMap, HashSet};
 use fs::Fs;
 use futures::future::Shared;
 use futures::{FutureExt, StreamExt as _};
-use git;
+use git::repository::DiffType;
 use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
 use language_model::{
     LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
@@ -19,7 +19,7 @@ use language_model::{
     LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
     Role, StopReason, TokenUsage,
 };
-use project::git_store::{GitStore, GitStoreCheckpoint};
+use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
 use project::{Project, Worktree};
 use prompt_store::{
     AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
@@ -1446,48 +1446,61 @@ impl Thread {
                 (path, snapshot)
             });
 
-            let Ok((worktree_path, snapshot)) = worktree_info else {
+            let Ok((worktree_path, _snapshot)) = worktree_info else {
                 return WorktreeSnapshot {
                     worktree_path: String::new(),
                     git_state: None,
                 };
             };
 
-            let repo_info = git_store
+            let git_state = git_store
                 .update(cx, |git_store, cx| {
                     git_store
                         .repositories()
                         .values()
-                        .find(|repo| repo.read(cx).worktree_id == Some(snapshot.id()))
-                        .and_then(|repo| {
-                            let repo = repo.read(cx);
-                            Some((repo.branch().cloned(), repo.local_repository()?))
+                        .find(|repo| {
+                            repo.read(cx)
+                                .abs_path_to_repo_path(&worktree.read(cx).abs_path())
+                                .is_some()
                         })
+                        .cloned()
                 })
                 .ok()
-                .flatten();
+                .flatten()
+                .map(|repo| {
+                    repo.read_with(cx, |repo, _| {
+                        let current_branch =
+                            repo.branch.as_ref().map(|branch| branch.name.to_string());
+                        repo.send_job(|state, _| async move {
+                            let RepositoryState::Local { backend, .. } = state else {
+                                return GitState {
+                                    remote_url: None,
+                                    head_sha: None,
+                                    current_branch,
+                                    diff: None,
+                                };
+                            };
+
+                            let remote_url = backend.remote_url("origin");
+                            let head_sha = backend.head_sha();
+                            let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
+
+                            GitState {
+                                remote_url,
+                                head_sha,
+                                current_branch,
+                                diff,
+                            }
+                        })
+                    })
+                });
 
-            // Extract git information
-            let git_state = match repo_info {
+            let git_state = match git_state {
+                Some(git_state) => match git_state.ok() {
+                    Some(git_state) => git_state.await.ok(),
+                    None => None,
+                },
                 None => None,
-                Some((branch, repo)) => {
-                    let current_branch = branch.map(|branch| branch.name.to_string());
-                    let remote_url = repo.remote_url("origin");
-                    let head_sha = repo.head_sha();
-
-                    // Get diff asynchronously
-                    let diff = repo
-                        .diff(git::repository::DiffType::HeadToWorktree)
-                        .await
-                        .ok();
-
-                    Some(GitState {
-                        remote_url,
-                        head_sha,
-                        current_branch,
-                        diff,
-                    })
-                }
             };
 
             WorktreeSnapshot {

crates/call/src/call_impl/room.rs 🔗

@@ -469,7 +469,7 @@ impl Room {
                         let repository = repository.read(cx);
                         repositories.push(proto::RejoinRepository {
                             id: entry_id.to_proto(),
-                            scan_id: repository.completed_scan_id as u64,
+                            scan_id: repository.scan_id,
                         });
                     }
 

crates/collab/src/db/queries/projects.rs 🔗

@@ -334,7 +334,7 @@ impl Database {
                             project_repository::ActiveModel {
                                 project_id: ActiveValue::set(project_id),
                                 legacy_worktree_id: ActiveValue::set(Some(worktree_id)),
-                                id: ActiveValue::set(repository.work_directory_id as i64),
+                                id: ActiveValue::set(repository.repository_id as i64),
                                 scan_id: ActiveValue::set(update.scan_id as i64),
                                 is_deleted: ActiveValue::set(false),
                                 branch_summary: ActiveValue::Set(
@@ -384,7 +384,7 @@ impl Database {
                                         project_repository_statuses::ActiveModel {
                                             project_id: ActiveValue::set(project_id),
                                             repository_id: ActiveValue::set(
-                                                repository.work_directory_id as i64,
+                                                repository.repository_id as i64,
                                             ),
                                             scan_id: ActiveValue::set(update.scan_id as i64),
                                             is_deleted: ActiveValue::set(false),
@@ -424,7 +424,7 @@ impl Database {
                                         .eq(project_id)
                                         .and(
                                             project_repository_statuses::Column::RepositoryId
-                                                .eq(repo.work_directory_id),
+                                                .eq(repo.repository_id),
                                         )
                                         .and(
                                             project_repository_statuses::Column::RepoPath
@@ -936,7 +936,7 @@ impl Database {
                         worktree.legacy_repository_entries.insert(
                             db_repository_entry.id as u64,
                             proto::RepositoryEntry {
-                                work_directory_id: db_repository_entry.id as u64,
+                                repository_id: db_repository_entry.id as u64,
                                 updated_statuses,
                                 removed_statuses: Vec::new(),
                                 current_merge_conflicts,
@@ -955,6 +955,7 @@ impl Database {
                         current_merge_conflicts,
                         branch_summary,
                         scan_id: db_repository_entry.scan_id as u64,
+                        is_last_update: true,
                     });
                 }
             }

crates/collab/src/db/queries/rooms.rs 🔗

@@ -764,7 +764,7 @@ impl Database {
                             .find(|worktree| worktree.id as i64 == legacy_worktree_id)
                         {
                             worktree.updated_repositories.push(proto::RepositoryEntry {
-                                work_directory_id: db_repository.id as u64,
+                                repository_id: db_repository.id as u64,
                                 updated_statuses,
                                 removed_statuses,
                                 current_merge_conflicts,
@@ -782,6 +782,7 @@ impl Database {
                             id: db_repository.id as u64,
                             abs_path: db_repository.abs_path,
                             scan_id: db_repository.scan_id as u64,
+                            is_last_update: true,
                         });
                     }
                 }

crates/collab/src/tests/integration_tests.rs 🔗

@@ -2898,8 +2898,8 @@ async fn test_git_branch_name(
         assert_eq!(
             repository
                 .read(cx)
-                .repository_entry
-                .branch()
+                .branch
+                .as_ref()
                 .map(|branch| branch.name.to_string()),
             branch_name
         )
@@ -3033,7 +3033,6 @@ async fn test_git_status_sync(
         let repo = repos.into_iter().next().unwrap();
         assert_eq!(
             repo.read(cx)
-                .repository_entry
                 .status_for_path(&file.into())
                 .map(|entry| entry.status),
             status
@@ -6882,7 +6881,8 @@ async fn test_remote_git_branches(
                 .next()
                 .unwrap()
                 .read(cx)
-                .current_branch()
+                .branch
+                .as_ref()
                 .unwrap()
                 .clone()
         })
@@ -6919,7 +6919,8 @@ async fn test_remote_git_branches(
                 .next()
                 .unwrap()
                 .read(cx)
-                .current_branch()
+                .branch
+                .as_ref()
                 .unwrap()
                 .clone()
         })

crates/collab/src/tests/random_project_collaboration_tests.rs 🔗

@@ -1181,6 +1181,10 @@ impl RandomizedTest for ProjectCollaborationTest {
                                         (worktree.id(), worktree.snapshot())
                                     })
                                     .collect::<BTreeMap<_, _>>();
+                                let host_repository_snapshots = host_project.read_with(host_cx, |host_project, cx| {
+                                    host_project.git_store().read(cx).repo_snapshots(cx)
+                                });
+                                let guest_repository_snapshots = guest_project.git_store().read(cx).repo_snapshots(cx);
 
                                 assert_eq!(
                                     guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
@@ -1189,6 +1193,13 @@ impl RandomizedTest for ProjectCollaborationTest {
                                     client.username, guest_project.remote_id(),
                                 );
 
+                                assert_eq!(
+                                    guest_repository_snapshots.values().collect::<Vec<_>>(),
+                                    host_repository_snapshots.values().collect::<Vec<_>>(),
+                                    "{} has different repositories than the host for project {:?}",
+                                    client.username, guest_project.remote_id(),
+                                );
+
                                 for (id, host_snapshot) in &host_worktree_snapshots {
                                     let guest_snapshot = &guest_worktree_snapshots[id];
                                     assert_eq!(
@@ -1216,12 +1227,6 @@ impl RandomizedTest for ProjectCollaborationTest {
                                         id,
                                         guest_project.remote_id(),
                                     );
-                                    assert_eq!(guest_snapshot.repositories().iter().collect::<Vec<_>>(), host_snapshot.repositories().iter().collect::<Vec<_>>(),
-                                        "{} has different repositories than the host for worktree {:?} and project {:?}",
-                                        client.username,
-                                        host_snapshot.abs_path(),
-                                        guest_project.remote_id(),
-                                    );
                                     assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
                                         "{} has different scan id than the host for worktree {:?} and project {:?}",
                                         client.username,

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -313,7 +313,8 @@ async fn test_ssh_collaboration_git_branches(
                     .next()
                     .unwrap()
                     .read(cx)
-                    .current_branch()
+                    .branch
+                    .as_ref()
                     .unwrap()
                     .clone()
             })
@@ -352,7 +353,8 @@ async fn test_ssh_collaboration_git_branches(
                     .next()
                     .unwrap()
                     .read(cx)
-                    .current_branch()
+                    .branch
+                    .as_ref()
                     .unwrap()
                     .clone()
             })

crates/editor/src/git/blame.rs 🔗

@@ -12,7 +12,10 @@ use gpui::{
 };
 use language::{Bias, Buffer, BufferSnapshot, Edit};
 use multi_buffer::RowInfo;
-use project::{Project, ProjectItem, git_store::Repository};
+use project::{
+    Project, ProjectItem,
+    git_store::{GitStoreEvent, Repository, RepositoryEvent},
+};
 use smallvec::SmallVec;
 use std::{sync::Arc, time::Duration};
 use sum_tree::SumTree;
@@ -202,13 +205,21 @@ impl GitBlame {
                         this.generate(cx);
                     }
                 }
-                project::Event::GitStateUpdated => {
+                _ => {}
+            }
+        });
+
+        let git_store = project.read(cx).git_store().clone();
+        let git_store_subscription =
+            cx.subscribe(&git_store, move |this, _, event, cx| match event {
+                GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, _)
+                | GitStoreEvent::RepositoryAdded(_)
+                | GitStoreEvent::RepositoryRemoved(_) => {
                     log::debug!("Status of git repositories updated. Regenerating blame data...",);
                     this.generate(cx);
                 }
                 _ => {}
-            }
-        });
+            });
 
         let buffer_snapshot = buffer.read(cx).snapshot();
         let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
@@ -226,7 +237,11 @@ impl GitBlame {
             task: Task::ready(Ok(())),
             generated: false,
             regenerate_on_edit_task: Task::ready(Ok(())),
-            _regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
+            _regenerate_subscriptions: vec![
+                buffer_subscriptions,
+                project_subscription,
+                git_store_subscription,
+            ],
         };
         this.generate(cx);
         this

crates/fs/src/fake_git_repo.rs 🔗

@@ -123,7 +123,7 @@ impl GitRepository for FakeGitRepository {
         &self,
         path: RepoPath,
         content: Option<String>,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<anyhow::Result<()>> {
         self.with_state_async(true, move |state| {
             if let Some(message) = state.simulated_index_write_error_message.clone() {
@@ -157,7 +157,7 @@ impl GitRepository for FakeGitRepository {
         &self,
         _commit: String,
         _mode: ResetMode,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -166,7 +166,7 @@ impl GitRepository for FakeGitRepository {
         &self,
         _commit: String,
         _paths: Vec<RepoPath>,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -179,7 +179,11 @@ impl GitRepository for FakeGitRepository {
         self.path()
     }
 
-    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
+    fn merge_message(&self) -> BoxFuture<Option<String>> {
+        async move { None }.boxed()
+    }
+
+    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
         let workdir_path = self.dot_git_path.parent().unwrap();
 
         // Load gitignores
@@ -221,7 +225,7 @@ impl GitRepository for FakeGitRepository {
             })
             .collect();
 
-        self.fs.with_git_state(&self.dot_git_path, false, |state| {
+        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
             let mut entries = Vec::new();
             let paths = state
                 .head_contents
@@ -302,10 +306,11 @@ impl GitRepository for FakeGitRepository {
                 }
             }
             entries.sort_by(|a, b| a.0.cmp(&b.0));
-            Ok(GitStatus {
+            anyhow::Ok(GitStatus {
                 entries: entries.into(),
             })
-        })?
+        });
+        async move { result? }.boxed()
     }
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
@@ -351,7 +356,7 @@ impl GitRepository for FakeGitRepository {
     fn stage_paths(
         &self,
         _paths: Vec<RepoPath>,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -359,7 +364,7 @@ impl GitRepository for FakeGitRepository {
     fn unstage_paths(
         &self,
         _paths: Vec<RepoPath>,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -368,7 +373,7 @@ impl GitRepository for FakeGitRepository {
         &self,
         _message: gpui::SharedString,
         _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         unimplemented!()
     }
@@ -379,7 +384,7 @@ impl GitRepository for FakeGitRepository {
         _remote: String,
         _options: Option<PushOptions>,
         _askpass: AskPassDelegate,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
         _cx: AsyncApp,
     ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
         unimplemented!()
@@ -390,7 +395,7 @@ impl GitRepository for FakeGitRepository {
         _branch: String,
         _remote: String,
         _askpass: AskPassDelegate,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
         _cx: AsyncApp,
     ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
         unimplemented!()
@@ -399,7 +404,7 @@ impl GitRepository for FakeGitRepository {
     fn fetch(
         &self,
         _askpass: AskPassDelegate,
-        _env: HashMap<String, String>,
+        _env: Arc<HashMap<String, String>>,
         _cx: AsyncApp,
     ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
         unimplemented!()

crates/git/src/repository.rs 🔗

@@ -188,7 +188,7 @@ pub trait GitRepository: Send + Sync {
         &self,
         path: RepoPath,
         content: Option<String>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<anyhow::Result<()>>;
 
     /// Returns the URL of the remote with the given name.
@@ -199,7 +199,9 @@ pub trait GitRepository: Send + Sync {
 
     fn merge_head_shas(&self) -> Vec<String>;
 
-    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
+    fn merge_message(&self) -> BoxFuture<Option<String>>;
+
+    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>;
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
 
@@ -210,14 +212,14 @@ pub trait GitRepository: Send + Sync {
         &self,
         commit: String,
         mode: ResetMode,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>>;
 
     fn checkout_files(
         &self,
         commit: String,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>>;
 
     fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
@@ -243,7 +245,7 @@ pub trait GitRepository: Send + Sync {
     fn stage_paths(
         &self,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>>;
     /// Updates the index to match HEAD at the given paths.
     ///
@@ -251,14 +253,14 @@ pub trait GitRepository: Send + Sync {
     fn unstage_paths(
         &self,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>>;
 
     fn commit(
         &self,
         message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>>;
 
     fn push(
@@ -267,7 +269,7 @@ pub trait GitRepository: Send + Sync {
         upstream_name: String,
         options: Option<PushOptions>,
         askpass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         // This method takes an AsyncApp to ensure it's invoked on the main thread,
         // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
@@ -278,7 +280,7 @@ pub trait GitRepository: Send + Sync {
         branch_name: String,
         upstream_name: String,
         askpass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         // This method takes an AsyncApp to ensure it's invoked on the main thread,
         // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
@@ -287,7 +289,7 @@ pub trait GitRepository: Send + Sync {
     fn fetch(
         &self,
         askpass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         // This method takes an AsyncApp to ensure it's invoked on the main thread,
         // otherwise git-credentials-manager won't work.
         cx: AsyncApp,
@@ -528,7 +530,7 @@ impl GitRepository for RealGitRepository {
         &self,
         commit: String,
         mode: ResetMode,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         async move {
             let working_directory = self.working_directory();
@@ -539,7 +541,7 @@ impl GitRepository for RealGitRepository {
             };
 
             let output = new_smol_command(&self.git_binary_path)
-                .envs(env)
+                .envs(env.iter())
                 .current_dir(&working_directory?)
                 .args(["reset", mode_flag, &commit])
                 .output()
@@ -559,7 +561,7 @@ impl GitRepository for RealGitRepository {
         &self,
         commit: String,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
@@ -570,7 +572,7 @@ impl GitRepository for RealGitRepository {
 
             let output = new_smol_command(&git_binary_path)
                 .current_dir(&working_directory?)
-                .envs(env)
+                .envs(env.iter())
                 .args(["checkout", &commit, "--"])
                 .args(paths.iter().map(|path| path.as_ref()))
                 .output()
@@ -640,7 +642,7 @@ impl GitRepository for RealGitRepository {
         &self,
         path: RepoPath,
         content: Option<String>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<anyhow::Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
@@ -650,7 +652,7 @@ impl GitRepository for RealGitRepository {
                 if let Some(content) = content {
                     let mut child = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory)
-                        .envs(&env)
+                        .envs(env.iter())
                         .args(["hash-object", "-w", "--stdin"])
                         .stdin(Stdio::piped())
                         .stdout(Stdio::piped())
@@ -668,7 +670,7 @@ impl GitRepository for RealGitRepository {
 
                     let output = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory)
-                        .envs(env)
+                        .envs(env.iter())
                         .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
                         .arg(path.to_unix_style())
                         .output()
@@ -683,7 +685,7 @@ impl GitRepository for RealGitRepository {
                 } else {
                     let output = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory)
-                        .envs(env)
+                        .envs(env.iter())
                         .args(["update-index", "--force-remove"])
                         .arg(path.to_unix_style())
                         .output()
@@ -733,18 +735,30 @@ impl GitRepository for RealGitRepository {
         shas
     }
 
-    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
-        let output = new_std_command(&self.git_binary_path)
-            .current_dir(self.working_directory()?)
-            .args(git_status_args(path_prefixes))
-            .output()?;
-        if output.status.success() {
-            let stdout = String::from_utf8_lossy(&output.stdout);
-            stdout.parse()
-        } else {
-            let stderr = String::from_utf8_lossy(&output.stderr);
-            Err(anyhow!("git status failed: {}", stderr))
-        }
+    fn merge_message(&self) -> BoxFuture<Option<String>> {
+        let path = self.path().join("MERGE_MSG");
+        async move { std::fs::read_to_string(&path).ok() }.boxed()
+    }
+
+    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
+        let git_binary_path = self.git_binary_path.clone();
+        let working_directory = self.working_directory();
+        let path_prefixes = path_prefixes.to_owned();
+        self.executor
+            .spawn(async move {
+                let output = new_std_command(&git_binary_path)
+                    .current_dir(working_directory?)
+                    .args(git_status_args(&path_prefixes))
+                    .output()?;
+                if output.status.success() {
+                    let stdout = String::from_utf8_lossy(&output.stdout);
+                    stdout.parse()
+                } else {
+                    let stderr = String::from_utf8_lossy(&output.stderr);
+                    Err(anyhow!("git status failed: {}", stderr))
+                }
+            })
+            .boxed()
     }
 
     fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
@@ -891,7 +905,7 @@ impl GitRepository for RealGitRepository {
     fn stage_paths(
         &self,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
@@ -900,7 +914,7 @@ impl GitRepository for RealGitRepository {
                 if !paths.is_empty() {
                     let output = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory?)
-                        .envs(env)
+                        .envs(env.iter())
                         .args(["update-index", "--add", "--remove", "--"])
                         .args(paths.iter().map(|p| p.to_unix_style()))
                         .output()
@@ -921,7 +935,7 @@ impl GitRepository for RealGitRepository {
     fn unstage_paths(
         &self,
         paths: Vec<RepoPath>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         let git_binary_path = self.git_binary_path.clone();
@@ -931,7 +945,7 @@ impl GitRepository for RealGitRepository {
                 if !paths.is_empty() {
                     let output = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory?)
-                        .envs(env)
+                        .envs(env.iter())
                         .args(["reset", "--quiet", "--"])
                         .args(paths.iter().map(|p| p.as_ref()))
                         .output()
@@ -953,14 +967,14 @@ impl GitRepository for RealGitRepository {
         &self,
         message: SharedString,
         name_and_email: Option<(SharedString, SharedString)>,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
     ) -> BoxFuture<Result<()>> {
         let working_directory = self.working_directory();
         self.executor
             .spawn(async move {
                 let mut cmd = new_smol_command("git");
                 cmd.current_dir(&working_directory?)
-                    .envs(env)
+                    .envs(env.iter())
                     .args(["commit", "--quiet", "-m"])
                     .arg(&message.to_string())
                     .arg("--cleanup=strip");
@@ -988,7 +1002,7 @@ impl GitRepository for RealGitRepository {
         remote_name: String,
         options: Option<PushOptions>,
         ask_pass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>> {
         let working_directory = self.working_directory();
@@ -997,7 +1011,7 @@ impl GitRepository for RealGitRepository {
             let working_directory = working_directory?;
             let mut command = new_smol_command("git");
             command
-                .envs(&env)
+                .envs(env.iter())
                 .env("GIT_HTTP_USER_AGENT", "Zed")
                 .current_dir(&working_directory)
                 .args(["push"])
@@ -1021,7 +1035,7 @@ impl GitRepository for RealGitRepository {
         branch_name: String,
         remote_name: String,
         ask_pass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>> {
         let working_directory = self.working_directory();
@@ -1029,7 +1043,7 @@ impl GitRepository for RealGitRepository {
         async move {
             let mut command = new_smol_command("git");
             command
-                .envs(&env)
+                .envs(env.iter())
                 .env("GIT_HTTP_USER_AGENT", "Zed")
                 .current_dir(&working_directory?)
                 .args(["pull"])
@@ -1046,7 +1060,7 @@ impl GitRepository for RealGitRepository {
     fn fetch(
         &self,
         ask_pass: AskPassDelegate,
-        env: HashMap<String, String>,
+        env: Arc<HashMap<String, String>>,
         cx: AsyncApp,
     ) -> BoxFuture<Result<RemoteCommandOutput>> {
         let working_directory = self.working_directory();
@@ -1054,7 +1068,7 @@ impl GitRepository for RealGitRepository {
         async move {
             let mut command = new_smol_command("git");
             command
-                .envs(&env)
+                .envs(env.iter())
                 .env("GIT_HTTP_USER_AGENT", "Zed")
                 .current_dir(&working_directory?)
                 .args(["fetch", "--all"])
@@ -1467,7 +1481,7 @@ struct GitBinaryCommandError {
 }
 
 async fn run_git_command(
-    env: HashMap<String, String>,
+    env: Arc<HashMap<String, String>>,
     ask_pass: AskPassDelegate,
     mut command: smol::process::Command,
     executor: &BackgroundExecutor,
@@ -1769,12 +1783,19 @@ mod tests {
 
         let repo =
             RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
-        repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
-            .await
-            .unwrap();
-        repo.commit("Initial commit".into(), None, checkpoint_author_envs())
-            .await
-            .unwrap();
+        repo.stage_paths(
+            vec![RepoPath::from_str("file")],
+            Arc::new(HashMap::default()),
+        )
+        .await
+        .unwrap();
+        repo.commit(
+            "Initial commit".into(),
+            None,
+            Arc::new(checkpoint_author_envs()),
+        )
+        .await
+        .unwrap();
 
         smol::fs::write(&file_path, "modified before checkpoint")
             .await
@@ -1791,13 +1812,16 @@ mod tests {
         smol::fs::write(&file_path, "modified after checkpoint")
             .await
             .unwrap();
-        repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
-            .await
-            .unwrap();
+        repo.stage_paths(
+            vec![RepoPath::from_str("file")],
+            Arc::new(HashMap::default()),
+        )
+        .await
+        .unwrap();
         repo.commit(
             "Commit after checkpoint".into(),
             None,
-            checkpoint_author_envs(),
+            Arc::new(checkpoint_author_envs()),
         )
         .await
         .unwrap();
@@ -1889,12 +1913,19 @@ mod tests {
 
         let repo =
             RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
-        repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
-            .await
-            .unwrap();
-        repo.commit("Initial commit".into(), None, checkpoint_author_envs())
-            .await
-            .unwrap();
+        repo.stage_paths(
+            vec![RepoPath::from_str("file")],
+            Arc::new(HashMap::default()),
+        )
+        .await
+        .unwrap();
+        repo.commit(
+            "Initial commit".into(),
+            None,
+            Arc::new(checkpoint_author_envs()),
+        )
+        .await
+        .unwrap();
 
         let initial_commit_sha = repo.head_sha().unwrap();
 
@@ -1912,13 +1943,17 @@ mod tests {
                 RepoPath::from_str("new_file1"),
                 RepoPath::from_str("new_file2"),
             ],
-            HashMap::default(),
+            Arc::new(HashMap::default()),
+        )
+        .await
+        .unwrap();
+        repo.commit(
+            "Commit new files".into(),
+            None,
+            Arc::new(checkpoint_author_envs()),
         )
         .await
         .unwrap();
-        repo.commit("Commit new files".into(), None, checkpoint_author_envs())
-            .await
-            .unwrap();
 
         repo.restore_checkpoint(checkpoint).await.unwrap();
         assert_eq!(repo.head_sha().unwrap(), initial_commit_sha);
@@ -1935,7 +1970,7 @@ mod tests {
             "content2"
         );
         assert_eq!(
-            repo.status_blocking(&[]).unwrap().entries.as_ref(),
+            repo.status(&[]).await.unwrap().entries.as_ref(),
             &[
                 (RepoPath::from_str("new_file1"), FileStatus::Untracked),
                 (RepoPath::from_str("new_file2"), FileStatus::Untracked)

crates/git_ui/src/branch_picker.rs 🔗

@@ -336,7 +336,7 @@ impl PickerDelegate for BranchListDelegate {
 
         let current_branch = self.repo.as_ref().map(|repo| {
             repo.update(cx, |repo, _| {
-                repo.current_branch().map(|branch| branch.name.clone())
+                repo.branch.as_ref().map(|branch| branch.name.clone())
             })
         });
 
@@ -463,7 +463,7 @@ impl PickerDelegate for BranchListDelegate {
                                 let message = if entry.is_new {
                                     if let Some(current_branch) =
                                         self.repo.as_ref().and_then(|repo| {
-                                            repo.read(cx).current_branch().map(|b| b.name.clone())
+                                            repo.read(cx).branch.as_ref().map(|b| b.name.clone())
                                         })
                                     {
                                         format!("based off {}", current_branch)

crates/git_ui/src/commit_modal.rs 🔗

@@ -234,7 +234,7 @@ impl CommitModal {
 
         let branch = active_repo
             .as_ref()
-            .and_then(|repo| repo.read(cx).repository_entry.branch())
+            .and_then(|repo| repo.read(cx).branch.as_ref())
             .map(|b| b.name.clone())
             .unwrap_or_else(|| "<no branch>".into());
 

crates/git_ui/src/git_panel.rs 🔗

@@ -45,9 +45,10 @@ use panel::{
     PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
     panel_icon_button,
 };
+use project::git_store::RepositoryEvent;
 use project::{
     Fs, Project, ProjectPath,
-    git_store::{GitEvent, Repository},
+    git_store::{GitStoreEvent, Repository},
 };
 use serde::{Deserialize, Serialize};
 use settings::{Settings as _, SettingsStore};
@@ -340,7 +341,7 @@ const MAX_PANEL_EDITOR_LINES: usize = 6;
 
 pub(crate) fn commit_message_editor(
     commit_message_buffer: Entity<Buffer>,
-    placeholder: Option<&str>,
+    placeholder: Option<SharedString>,
     project: Entity<Project>,
     in_panel: bool,
     window: &mut Window,
@@ -361,7 +362,7 @@ pub(crate) fn commit_message_editor(
     commit_editor.set_show_wrap_guides(false, cx);
     commit_editor.set_show_indent_guides(false, cx);
     commit_editor.set_hard_wrap(Some(72), cx);
-    let placeholder = placeholder.unwrap_or("Enter commit message");
+    let placeholder = placeholder.unwrap_or("Enter commit message".into());
     commit_editor.set_placeholder_text(placeholder, cx);
     commit_editor
 }
@@ -403,14 +404,18 @@ impl GitPanel {
             &git_store,
             window,
             move |this, git_store, event, window, cx| match event {
-                GitEvent::FileSystemUpdated => {
-                    this.schedule_update(false, window, cx);
-                }
-                GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
+                GitStoreEvent::ActiveRepositoryChanged(_) => {
                     this.active_repository = git_store.read(cx).active_repository();
                     this.schedule_update(true, window, cx);
                 }
-                GitEvent::IndexWriteError(error) => {
+                GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, true) => {
+                    this.schedule_update(true, window, cx);
+                }
+                GitStoreEvent::RepositoryUpdated(_, _, _) => {}
+                GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
+                    this.schedule_update(false, window, cx);
+                }
+                GitStoreEvent::IndexWriteError(error) => {
                     this.workspace
                         .update(cx, |workspace, cx| {
                             workspace.show_error(error, cx);
@@ -828,7 +833,7 @@ impl GitPanel {
             .active_repository
             .as_ref()
             .map_or(false, |active_repository| {
-                active_repository.read(cx).entry_count() > 0
+                active_repository.read(cx).status_summary().count > 0
             });
         if have_entries && self.selected_entry.is_none() {
             self.selected_entry = Some(1);
@@ -1415,7 +1420,7 @@ impl GitPanel {
         let message = self.commit_editor.read(cx).text(cx);
 
         if !message.trim().is_empty() {
-            return Some(message.to_string());
+            return Some(message);
         }
 
         self.suggest_commit_message(cx)
@@ -1593,7 +1598,7 @@ impl GitPanel {
             .as_ref()
             .and_then(|repo| repo.read(cx).merge_message.as_ref())
         {
-            return Some(merge_message.clone());
+            return Some(merge_message.to_string());
         }
 
         let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
@@ -1849,7 +1854,7 @@ impl GitPanel {
         let Some(repo) = self.active_repository.clone() else {
             return;
         };
-        let Some(branch) = repo.read(cx).current_branch() else {
+        let Some(branch) = repo.read(cx).branch.as_ref() else {
             return;
         };
         telemetry::event!("Git Pulled");
@@ -1906,7 +1911,7 @@ impl GitPanel {
         let Some(repo) = self.active_repository.clone() else {
             return;
         };
-        let Some(branch) = repo.read(cx).current_branch() else {
+        let Some(branch) = repo.read(cx).branch.as_ref() else {
             return;
         };
         telemetry::event!("Git Pushed");
@@ -2019,7 +2024,7 @@ impl GitPanel {
 
             let mut current_remotes: Vec<Remote> = repo
                 .update(&mut cx, |repo, _| {
-                    let Some(current_branch) = repo.current_branch() else {
+                    let Some(current_branch) = repo.branch.as_ref() else {
                         return Err(anyhow::anyhow!("No active branch"));
                     };
 
@@ -2215,7 +2220,7 @@ impl GitPanel {
                     git_panel.commit_editor = cx.new(|cx| {
                         commit_message_editor(
                             buffer,
-                            git_panel.suggest_commit_message(cx).as_deref(),
+                            git_panel.suggest_commit_message(cx).map(SharedString::from),
                             git_panel.project.clone(),
                             true,
                             window,
@@ -2275,10 +2280,7 @@ impl GitPanel {
                 continue;
             }
 
-            let abs_path = repo
-                .repository_entry
-                .work_directory_abs_path
-                .join(&entry.repo_path.0);
+            let abs_path = repo.work_directory_abs_path.join(&entry.repo_path.0);
             let entry = GitStatusEntry {
                 repo_path: entry.repo_path.clone(),
                 abs_path,
@@ -2392,9 +2394,7 @@ impl GitPanel {
         self.select_first_entry_if_none(cx);
 
         let suggested_commit_message = self.suggest_commit_message(cx);
-        let placeholder_text = suggested_commit_message
-            .as_deref()
-            .unwrap_or("Enter commit message");
+        let placeholder_text = suggested_commit_message.unwrap_or("Enter commit message".into());
 
         self.commit_editor.update(cx, |editor, cx| {
             editor.set_placeholder_text(Arc::from(placeholder_text), cx)
@@ -2823,12 +2823,7 @@ impl GitPanel {
     }
 
     pub(crate) fn render_remote_button(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
-        let branch = self
-            .active_repository
-            .as_ref()?
-            .read(cx)
-            .current_branch()
-            .cloned();
+        let branch = self.active_repository.as_ref()?.read(cx).branch.clone();
         if !self.can_push_and_pull(cx) {
             return None;
         }
@@ -2868,7 +2863,7 @@ impl GitPanel {
         let commit_tooltip_focus_handle = editor_focus_handle.clone();
         let expand_tooltip_focus_handle = editor_focus_handle.clone();
 
-        let branch = active_repository.read(cx).current_branch().cloned();
+        let branch = active_repository.read(cx).branch.clone();
 
         let footer_size = px(32.);
         let gap = px(9.0);
@@ -2999,7 +2994,7 @@ impl GitPanel {
 
     fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
         let active_repository = self.active_repository.as_ref()?;
-        let branch = active_repository.read(cx).current_branch()?;
+        let branch = active_repository.read(cx).branch.as_ref()?;
         let commit = branch.most_recent_commit.as_ref()?.clone();
         let workspace = self.workspace.clone();
 

crates/git_ui/src/project_diff.rs 🔗

@@ -24,7 +24,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
 use multi_buffer::{MultiBuffer, PathKey};
 use project::{
     Project, ProjectPath,
-    git_store::{GitEvent, GitStore},
+    git_store::{GitStore, GitStoreEvent, RepositoryEvent},
 };
 use std::any::{Any, TypeId};
 use theme::ActiveTheme;
@@ -153,9 +153,8 @@ impl ProjectDiff {
             &git_store,
             window,
             move |this, _git_store, event, _window, _cx| match event {
-                GitEvent::ActiveRepositoryChanged
-                | GitEvent::FileSystemUpdated
-                | GitEvent::GitStateUpdated => {
+                GitStoreEvent::ActiveRepositoryChanged(_)
+                | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, true) => {
                     *this.update_needed.borrow_mut() = ();
                 }
                 _ => {}
@@ -452,13 +451,11 @@ impl ProjectDiff {
     ) -> Result<()> {
         while let Some(_) = recv.next().await {
             this.update(cx, |this, cx| {
-                let new_branch =
-                    this.git_store
-                        .read(cx)
-                        .active_repository()
-                        .and_then(|active_repository| {
-                            active_repository.read(cx).current_branch().cloned()
-                        });
+                let new_branch = this
+                    .git_store
+                    .read(cx)
+                    .active_repository()
+                    .and_then(|active_repository| active_repository.read(cx).branch.clone());
                 if new_branch != this.current_branch {
                     this.current_branch = new_branch;
                     cx.notify();
@@ -1499,6 +1496,7 @@ mod tests {
             .unindent(),
         );
 
+        eprintln!(">>>>>>>> git restore");
         let prev_buffer_hunks =
             cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
                 let snapshot = buffer_editor.snapshot(window, cx);
@@ -1516,14 +1514,13 @@ mod tests {
             cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
                 let snapshot = buffer_editor.snapshot(window, cx);
                 let snapshot = &snapshot.buffer_snapshot;
-                let new_buffer_hunks = buffer_editor
+                buffer_editor
                     .diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
-                    .collect::<Vec<_>>();
-                buffer_editor.git_restore(&Default::default(), window, cx);
-                new_buffer_hunks
+                    .collect::<Vec<_>>()
             });
         assert_eq!(new_buffer_hunks.as_slice(), &[]);
 
+        eprintln!(">>>>>>>> modify");
         cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
             buffer_editor.set_text("different\n", window, cx);
             buffer_editor.save(false, project.clone(), window, cx)
@@ -1533,6 +1530,20 @@ mod tests {
 
         cx.run_until_parked();
 
+        cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
+            buffer_editor.expand_all_diff_hunks(&Default::default(), window, cx);
+        });
+
+        assert_state_with_diff(
+            &buffer_editor,
+            cx,
+            &"
+                - original
+                + different
+                  ˇ"
+            .unindent(),
+        );
+
         assert_state_with_diff(
             &diff_editor,
             cx,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -2475,6 +2475,7 @@ impl MultiBuffer {
         let buffer_id = diff.buffer_id;
         let buffers = self.buffers.borrow();
         let Some(buffer_state) = buffers.get(&buffer_id) else {
+            eprintln!("no buffer");
             return;
         };
 

crates/project/Cargo.toml 🔗

@@ -43,6 +43,7 @@ fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true
+git_hosting_providers.workspace = true
 globset.workspace = true
 gpui.workspace = true
 http_client.workspace = true

crates/project/src/buffer_store.rs 🔗

@@ -872,21 +872,6 @@ impl BufferStore {
         cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
-    pub(crate) fn worktree_for_buffer(
-        &self,
-        buffer: &Entity<Buffer>,
-        cx: &App,
-    ) -> Option<(Entity<Worktree>, Arc<Path>)> {
-        let file = buffer.read(cx).file()?;
-        let worktree_id = file.worktree_id(cx);
-        let path = file.path().clone();
-        let worktree = self
-            .worktree_store
-            .read(cx)
-            .worktree_for_id(worktree_id, cx)?;
-        Some((worktree, path))
-    }
-
     pub fn create_buffer(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
         match &self.state {
             BufferStoreState::Local(this) => this.create_buffer(cx),

crates/project/src/connection_manager.rs 🔗

@@ -91,7 +91,7 @@ impl Manager {
                         for (id, repository) in project.repositories(cx) {
                             repositories.push(proto::RejoinRepository {
                                 id: id.to_proto(),
-                                scan_id: repository.read(cx).completed_scan_id as u64,
+                                scan_id: repository.read(cx).scan_id,
                             });
                         }
                         for worktree in project.worktrees(cx) {

crates/project/src/debugger/dap_store.rs 🔗

@@ -339,7 +339,7 @@ impl DapStore {
             local_store.toolchain_store.clone(),
             local_store.environment.update(cx, |env, cx| {
                 let worktree = worktree.read(cx);
-                env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
+                env.get_environment(worktree.abs_path().into(), cx)
             }),
         );
         let session_id = local_store.next_session_id();
@@ -407,7 +407,7 @@ impl DapStore {
             local_store.toolchain_store.clone(),
             local_store.environment.update(cx, |env, cx| {
                 let worktree = worktree.read(cx);
-                env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
+                env.get_environment(Some(worktree.abs_path()), cx)
             }),
         );
         let session_id = local_store.next_session_id();

crates/project/src/environment.rs 🔗

@@ -1,11 +1,13 @@
-use futures::{FutureExt, future::Shared};
+use futures::{
+    FutureExt,
+    future::{Shared, WeakShared},
+};
 use std::{path::Path, sync::Arc};
 use util::ResultExt;
 
 use collections::HashMap;
 use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
 use settings::Settings as _;
-use worktree::WorktreeId;
 
 use crate::{
     project_settings::{DirenvSettings, ProjectSettings},
@@ -13,10 +15,9 @@ use crate::{
 };
 
 pub struct ProjectEnvironment {
-    worktree_store: Entity<WorktreeStore>,
     cli_environment: Option<HashMap<String, String>>,
-    environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
-    environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
+    environments: HashMap<Arc<Path>, WeakShared<Task<Option<HashMap<String, String>>>>>,
+    environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
 }
 
 pub enum ProjectEnvironmentEvent {
@@ -33,14 +34,15 @@ impl ProjectEnvironment {
     ) -> Entity<Self> {
         cx.new(|cx| {
             cx.subscribe(worktree_store, |this: &mut Self, _, event, _| {
-                if let WorktreeStoreEvent::WorktreeRemoved(_, id) = event {
-                    this.remove_worktree_environment(*id);
+                if let WorktreeStoreEvent::WorktreeRemoved(_, _) = event {
+                    this.environments.retain(|_, weak| weak.upgrade().is_some());
+                    this.environment_error_messages
+                        .retain(|abs_path, _| this.environments.contains_key(abs_path));
                 }
             })
             .detach();
 
             Self {
-                worktree_store: worktree_store.clone(),
                 cli_environment,
                 environments: Default::default(),
                 environment_error_messages: Default::default(),
@@ -48,11 +50,6 @@ impl ProjectEnvironment {
         })
     }
 
-    pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
-        self.environment_error_messages.remove(&worktree_id);
-        self.environments.remove(&worktree_id);
-    }
-
     /// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
     pub(crate) fn get_cli_environment(&self) -> Option<HashMap<String, String>> {
         if let Some(mut env) = self.cli_environment.clone() {
@@ -67,28 +64,22 @@ impl ProjectEnvironment {
     /// environment errors associated with this project environment.
     pub(crate) fn environment_errors(
         &self,
-    ) -> impl Iterator<Item = (&WorktreeId, &EnvironmentErrorMessage)> {
+    ) -> impl Iterator<Item = (&Arc<Path>, &EnvironmentErrorMessage)> {
         self.environment_error_messages.iter()
     }
 
-    pub(crate) fn remove_environment_error(
-        &mut self,
-        worktree_id: WorktreeId,
-        cx: &mut Context<Self>,
-    ) {
-        self.environment_error_messages.remove(&worktree_id);
+    pub(crate) fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
+        self.environment_error_messages.remove(abs_path);
         cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
     }
 
     /// Returns the project environment, if possible.
     /// If the project was opened from the CLI, then the inherited CLI environment is returned.
-    /// If it wasn't opened from the CLI, and a worktree is given, then a shell is spawned in
-    /// the worktree's path, to get environment variables as if the user has `cd`'d into
-    /// the worktrees path.
+    /// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
+    /// that directory, to get environment variables as if the user has `cd`'d there.
     pub(crate) fn get_environment(
         &mut self,
-        worktree_id: Option<WorktreeId>,
-        worktree_abs_path: Option<Arc<Path>>,
+        abs_path: Option<Arc<Path>>,
         cx: &Context<Self>,
     ) -> Shared<Task<Option<HashMap<String, String>>>> {
         if cfg!(any(test, feature = "test-support")) {
@@ -111,74 +102,26 @@ impl ProjectEnvironment {
                 .shared();
         }
 
-        let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
+        let Some(abs_path) = abs_path else {
             return Task::ready(None).shared();
         };
 
-        if self
-            .worktree_store
-            .read(cx)
-            .worktree_for_id(worktree_id, cx)
-            .map(|w| !w.read(cx).is_local())
-            .unwrap_or(true)
+        if let Some(existing) = self
+            .environments
+            .get(&abs_path)
+            .and_then(|weak| weak.upgrade())
         {
-            return Task::ready(None).shared();
-        }
-
-        if let Some(task) = self.environments.get(&worktree_id) {
-            task.clone()
+            existing
         } else {
-            let task = self
-                .get_worktree_env(worktree_id, worktree_abs_path, cx)
-                .shared();
-            self.environments.insert(worktree_id, task.clone());
-            task
+            let env = get_directory_env(abs_path.clone(), cx).shared();
+            self.environments.insert(
+                abs_path.clone(),
+                env.downgrade()
+                    .expect("environment task has not been polled yet"),
+            );
+            env
         }
     }
-
-    fn get_worktree_env(
-        &mut self,
-        worktree_id: WorktreeId,
-        worktree_abs_path: Arc<Path>,
-        cx: &Context<Self>,
-    ) -> Task<Option<HashMap<String, String>>> {
-        let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
-
-        cx.spawn(async move |this, cx| {
-            let (mut shell_env, error_message) = cx
-                .background_spawn({
-                    let worktree_abs_path = worktree_abs_path.clone();
-                    async move {
-                        load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
-                    }
-                })
-                .await;
-
-            if let Some(shell_env) = shell_env.as_mut() {
-                let path = shell_env
-                    .get("PATH")
-                    .map(|path| path.as_str())
-                    .unwrap_or_default();
-                log::info!(
-                    "using project environment variables shell launched in {:?}. PATH={:?}",
-                    worktree_abs_path,
-                    path
-                );
-
-                set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
-            }
-
-            if let Some(error) = error_message {
-                this.update(cx, |this, cx| {
-                    this.environment_error_messages.insert(worktree_id, error);
-                    cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
-                })
-                .log_err();
-            }
-
-            shell_env
-        })
-    }
 }
 
 fn set_origin_marker(env: &mut HashMap<String, String>, origin: EnvironmentOrigin) {
@@ -210,25 +153,25 @@ impl EnvironmentErrorMessage {
     }
 }
 
-async fn load_worktree_shell_environment(
-    worktree_abs_path: &Path,
+async fn load_directory_shell_environment(
+    abs_path: &Path,
     load_direnv: &DirenvSettings,
 ) -> (
     Option<HashMap<String, String>>,
     Option<EnvironmentErrorMessage>,
 ) {
-    match smol::fs::metadata(worktree_abs_path).await {
+    match smol::fs::metadata(abs_path).await {
         Ok(meta) => {
             let dir = if meta.is_dir() {
-                worktree_abs_path
-            } else if let Some(parent) = worktree_abs_path.parent() {
+                abs_path
+            } else if let Some(parent) = abs_path.parent() {
                 parent
             } else {
                 return (
                     None,
                     Some(EnvironmentErrorMessage(format!(
                         "Failed to load shell environment in {}: not a directory",
-                        worktree_abs_path.display()
+                        abs_path.display()
                     ))),
                 );
             };
@@ -239,7 +182,7 @@ async fn load_worktree_shell_environment(
             None,
             Some(EnvironmentErrorMessage(format!(
                 "Failed to load shell environment in {}: {}",
-                worktree_abs_path.display(),
+                abs_path.display(),
                 err
             ))),
         ),
@@ -387,3 +330,43 @@ async fn load_shell_environment(
 
     (Some(parsed_env), direnv_error)
 }
+
+fn get_directory_env(
+    abs_path: Arc<Path>,
+    cx: &Context<ProjectEnvironment>,
+) -> Task<Option<HashMap<String, String>>> {
+    let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
+
+    cx.spawn(async move |this, cx| {
+        let (mut shell_env, error_message) = cx
+            .background_spawn({
+                let abs_path = abs_path.clone();
+                async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
+            })
+            .await;
+
+        if let Some(shell_env) = shell_env.as_mut() {
+            let path = shell_env
+                .get("PATH")
+                .map(|path| path.as_str())
+                .unwrap_or_default();
+            log::info!(
+                "using project environment variables shell launched in {:?}. PATH={:?}",
+                abs_path,
+                path
+            );
+
+            set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
+        }
+
+        if let Some(error) = error_message {
+            this.update(cx, |this, cx| {
+                this.environment_error_messages.insert(abs_path, error);
+                cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
+            })
+            .log_err();
+        }
+
+        shell_env
+    })
+}

crates/project/src/git_store.rs 🔗

@@ -14,17 +14,20 @@ use fs::Fs;
 use futures::{
     FutureExt as _, StreamExt as _,
     channel::{mpsc, oneshot},
-    future::{self, OptionFuture, Shared},
+    future::{self, Shared},
 };
 use git::{
-    BuildPermalinkParams, GitHostingProviderRegistry,
+    BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
     blame::Blame,
     parse_git_remote_url,
     repository::{
         Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
         GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
+        UpstreamTrackingStatus,
+    },
+    status::{
+        FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
     },
-    status::FileStatus,
 };
 use gpui::{
     App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
@@ -37,38 +40,40 @@ use language::{
 use parking_lot::Mutex;
 use rpc::{
     AnyProtoClient, TypedEnvelope,
-    proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset},
+    proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
 };
 use serde::Deserialize;
-use settings::WorktreeId;
 use std::{
-    collections::{VecDeque, hash_map},
+    cmp::Ordering,
+    collections::{BTreeSet, VecDeque},
     future::Future,
+    mem,
     ops::Range,
     path::{Path, PathBuf},
-    sync::Arc,
+    sync::{
+        Arc,
+        atomic::{self, AtomicU64},
+    },
 };
-use sum_tree::TreeSet;
-use text::BufferId;
-use util::{ResultExt, debug_panic, maybe};
+use sum_tree::{Edit, SumTree, TreeSet};
+use text::{Bias, BufferId};
+use util::{ResultExt, debug_panic};
 use worktree::{
-    File, PathKey, ProjectEntryId, RepositoryEntry, StatusEntry, UpdatedGitRepositoriesSet,
-    Worktree, proto_to_branch,
+    File, PathKey, PathProgress, PathSummary, PathTarget, UpdatedGitRepositoriesSet, Worktree,
 };
 
 pub struct GitStore {
     state: GitStoreState,
     buffer_store: Entity<BufferStore>,
     worktree_store: Entity<WorktreeStore>,
-    repositories: HashMap<ProjectEntryId, Entity<Repository>>,
-    active_repo_id: Option<ProjectEntryId>,
+    repositories: HashMap<RepositoryId, Entity<Repository>>,
+    active_repo_id: Option<RepositoryId>,
     #[allow(clippy::type_complexity)]
     loading_diffs:
         HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
     diffs: HashMap<BufferId, Entity<BufferDiffState>>,
-    update_sender: mpsc::UnboundedSender<GitJob>,
     shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
-    _subscriptions: [Subscription; 2],
+    _subscriptions: Vec<Subscription>,
 }
 
 #[derive(Default)]
@@ -113,25 +118,25 @@ enum DiffKind {
 
 enum GitStoreState {
     Local {
-        downstream_client: Option<LocalDownstreamState>,
-        environment: Entity<ProjectEnvironment>,
+        next_repository_id: Arc<AtomicU64>,
+        downstream: Option<LocalDownstreamState>,
+        project_environment: Entity<ProjectEnvironment>,
         fs: Arc<dyn Fs>,
     },
     Ssh {
         upstream_client: AnyProtoClient,
         upstream_project_id: ProjectId,
-        downstream_client: Option<(AnyProtoClient, ProjectId)>,
-        environment: Entity<ProjectEnvironment>,
+        downstream: Option<(AnyProtoClient, ProjectId)>,
     },
     Remote {
         upstream_client: AnyProtoClient,
-        project_id: ProjectId,
+        upstream_project_id: ProjectId,
     },
 }
 
 enum DownstreamUpdate {
-    UpdateRepository(RepositoryEntry),
-    RemoveRepository(ProjectEntryId),
+    UpdateRepository(RepositorySnapshot),
+    RemoveRepository(RepositoryId),
 }
 
 struct LocalDownstreamState {
@@ -143,54 +148,145 @@ struct LocalDownstreamState {
 
 #[derive(Clone)]
 pub struct GitStoreCheckpoint {
-    checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
+    checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct StatusEntry {
+    pub repo_path: RepoPath,
+    pub status: FileStatus,
+}
+
+impl StatusEntry {
+    fn to_proto(&self) -> proto::StatusEntry {
+        let simple_status = match self.status {
+            FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
+            FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
+            FileStatus::Tracked(TrackedStatus {
+                index_status,
+                worktree_status,
+            }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
+                worktree_status
+            } else {
+                index_status
+            }),
+        };
+
+        proto::StatusEntry {
+            repo_path: self.repo_path.as_ref().to_proto(),
+            simple_status,
+            status: Some(status_to_proto(self.status)),
+        }
+    }
+}
+
+impl TryFrom<proto::StatusEntry> for StatusEntry {
+    type Error = anyhow::Error;
+
+    fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
+        let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
+        let status = status_from_proto(value.simple_status, value.status)?;
+        Ok(Self { repo_path, status })
+    }
+}
+
+impl sum_tree::Item for StatusEntry {
+    type Summary = PathSummary<GitSummary>;
+
+    fn summary(&self, _: &<Self::Summary as sum_tree::Summary>::Context) -> Self::Summary {
+        PathSummary {
+            max_path: self.repo_path.0.clone(),
+            item_summary: self.status.summary(),
+        }
+    }
+}
+
+impl sum_tree::KeyedItem for StatusEntry {
+    type Key = PathKey;
+
+    fn key(&self) -> Self::Key {
+        PathKey(self.repo_path.0.clone())
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RepositoryId(pub u64);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RepositorySnapshot {
+    pub id: RepositoryId,
+    pub merge_message: Option<SharedString>,
+    pub statuses_by_path: SumTree<StatusEntry>,
+    pub work_directory_abs_path: Arc<Path>,
+    pub branch: Option<Branch>,
+    pub merge_conflicts: TreeSet<RepoPath>,
+    pub merge_head_shas: Vec<SharedString>,
+    pub scan_id: u64,
 }
 
 pub struct Repository {
-    pub repository_entry: RepositoryEntry,
-    pub merge_message: Option<String>,
-    pub completed_scan_id: usize,
+    snapshot: RepositorySnapshot,
     commit_message_buffer: Option<Entity<Buffer>>,
     git_store: WeakEntity<GitStore>,
-    project_environment: Option<WeakEntity<ProjectEnvironment>>,
-    pub worktree_id: Option<WorktreeId>,
-    state: RepositoryState,
+    // For a local repository, holds paths that have had worktree events since the last status scan completed,
+    // and that should be examined during the next status scan.
+    paths_needing_status_update: BTreeSet<RepoPath>,
     job_sender: mpsc::UnboundedSender<GitJob>,
     askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
     latest_askpass_id: u64,
 }
 
+impl std::ops::Deref for Repository {
+    type Target = RepositorySnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
 #[derive(Clone)]
-enum RepositoryState {
-    Local(Arc<dyn GitRepository>),
+pub enum RepositoryState {
+    Local {
+        backend: Arc<dyn GitRepository>,
+        environment: Arc<HashMap<String, String>>,
+    },
     Remote {
         project_id: ProjectId,
         client: AnyProtoClient,
-        work_directory_id: ProjectEntryId,
     },
 }
 
+#[derive(Clone, Debug)]
+pub enum RepositoryEvent {
+    Updated,
+    MergeHeadsChanged,
+}
+
 #[derive(Debug)]
-pub enum GitEvent {
-    ActiveRepositoryChanged,
-    FileSystemUpdated,
-    GitStateUpdated,
+pub enum GitStoreEvent {
+    ActiveRepositoryChanged(Option<RepositoryId>),
+    RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
+    RepositoryAdded(RepositoryId),
+    RepositoryRemoved(RepositoryId),
     IndexWriteError(anyhow::Error),
 }
 
+impl EventEmitter<RepositoryEvent> for Repository {}
+impl EventEmitter<GitStoreEvent> for GitStore {}
+
 struct GitJob {
-    job: Box<dyn FnOnce(&mut AsyncApp) -> Task<()>>,
+    job: Box<dyn FnOnce(RepositoryState, &mut AsyncApp) -> Task<()>>,
     key: Option<GitJobKey>,
 }
 
 #[derive(PartialEq, Eq)]
 enum GitJobKey {
     WriteIndex(RepoPath),
-    BatchReadIndex(ProjectEntryId),
+    BatchReadIndex,
+    RefreshStatuses,
+    ReloadGitState,
 }
 
-impl EventEmitter<GitEvent> for GitStore {}
-
 impl GitStore {
     pub fn local(
         worktree_store: &Entity<WorktreeStore>,
@@ -203,8 +299,9 @@ impl GitStore {
             worktree_store.clone(),
             buffer_store,
             GitStoreState::Local {
-                downstream_client: None,
-                environment,
+                next_repository_id: Arc::new(AtomicU64::new(1)),
+                downstream: None,
+                project_environment: environment,
                 fs,
             },
             cx,
@@ -223,7 +320,7 @@ impl GitStore {
             buffer_store,
             GitStoreState::Remote {
                 upstream_client,
-                project_id,
+                upstream_project_id: project_id,
             },
             cx,
         )
@@ -232,7 +329,6 @@ impl GitStore {
     pub fn ssh(
         worktree_store: &Entity<WorktreeStore>,
         buffer_store: Entity<BufferStore>,
-        environment: Entity<ProjectEnvironment>,
         upstream_client: AnyProtoClient,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -242,8 +338,7 @@ impl GitStore {
             GitStoreState::Ssh {
                 upstream_client,
                 upstream_project_id: ProjectId(SSH_PROJECT_ID),
-                downstream_client: None,
-                environment,
+                downstream: None,
             },
             cx,
         )
@@ -255,8 +350,7 @@ impl GitStore {
         state: GitStoreState,
         cx: &mut Context<Self>,
     ) -> Self {
-        let update_sender = Self::spawn_git_worker(cx);
-        let _subscriptions = [
+        let _subscriptions = vec![
             cx.subscribe(&worktree_store, Self::on_worktree_store_event),
             cx.subscribe(&buffer_store, Self::on_buffer_store_event),
         ];
@@ -267,7 +361,6 @@ impl GitStore {
             worktree_store,
             repositories: HashMap::default(),
             active_repo_id: None,
-            update_sender,
             _subscriptions,
             loading_diffs: HashMap::default(),
             shared_diffs: HashMap::default(),
@@ -312,24 +405,27 @@ impl GitStore {
     pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
         match &mut self.state {
             GitStoreState::Ssh {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => {
                 for repo in self.repositories.values() {
-                    client
-                        .send(repo.read(cx).repository_entry.initial_update(project_id))
-                        .log_err();
+                    let update = repo.read(cx).snapshot.initial_update(project_id);
+                    for update in split_repository_update(update) {
+                        client.send(update).log_err();
+                    }
                 }
                 *downstream_client = Some((client, ProjectId(project_id)));
             }
             GitStoreState::Local {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => {
                 let mut snapshots = HashMap::default();
                 let (updates_tx, mut updates_rx) = mpsc::unbounded();
                 for repo in self.repositories.values() {
                     updates_tx
                         .unbounded_send(DownstreamUpdate::UpdateRepository(
-                            repo.read(cx).repository_entry.clone(),
+                            repo.read(cx).snapshot.clone(),
                         ))
                         .ok();
                 }
@@ -342,17 +438,20 @@ impl GitStore {
                             while let Some(update) = updates_rx.next().await {
                                 match update {
                                     DownstreamUpdate::UpdateRepository(snapshot) => {
-                                        if let Some(old_snapshot) =
-                                            snapshots.get_mut(&snapshot.work_directory_id)
+                                        if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id)
                                         {
                                             let update =
                                                 snapshot.build_update(old_snapshot, project_id);
                                             *old_snapshot = snapshot;
-                                            client.send(update)?;
+                                            for update in split_repository_update(update) {
+                                                client.send(update)?;
+                                            }
                                         } else {
                                             let update = snapshot.initial_update(project_id);
-                                            client.send(update)?;
-                                            snapshots.insert(snapshot.work_directory_id, snapshot);
+                                            for update in split_repository_update(update) {
+                                                client.send(update)?;
+                                            }
+                                            snapshots.insert(snapshot.id, snapshot);
                                         }
                                     }
                                     DownstreamUpdate::RemoveRepository(id) => {
@@ -369,7 +468,8 @@ impl GitStore {
                         .ok();
                         this.update(cx, |this, _| {
                             if let GitStoreState::Local {
-                                downstream_client, ..
+                                downstream: downstream_client,
+                                ..
                             } = &mut this.state
                             {
                                 downstream_client.take();
@@ -389,12 +489,14 @@ impl GitStore {
     pub fn unshared(&mut self, _cx: &mut Context<Self>) {
         match &mut self.state {
             GitStoreState::Local {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => {
                 downstream_client.take();
             }
             GitStoreState::Ssh {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => {
                 downstream_client.take();
             }
@@ -440,29 +542,32 @@ impl GitStore {
             }
         }
 
-        let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
-            hash_map::Entry::Occupied(e) => e.get().clone(),
-            hash_map::Entry::Vacant(entry) => {
-                let staged_text = self.state.load_staged_text(&buffer, &self.buffer_store, cx);
-                entry
-                    .insert(
-                        cx.spawn(async move |this, cx| {
-                            Self::open_diff_internal(
-                                this,
-                                DiffKind::Unstaged,
-                                staged_text.await.map(DiffBasesChange::SetIndex),
-                                buffer,
-                                cx,
-                            )
-                            .await
-                            .map_err(Arc::new)
-                        })
-                        .shared(),
-                    )
-                    .clone()
-            }
+        let Some((repo, repo_path)) =
+            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
+        else {
+            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
         };
 
+        let task = self
+            .loading_diffs
+            .entry((buffer_id, DiffKind::Unstaged))
+            .or_insert_with(|| {
+                let staged_text = repo.read(cx).load_staged_text(buffer_id, repo_path, cx);
+                cx.spawn(async move |this, cx| {
+                    Self::open_diff_internal(
+                        this,
+                        DiffKind::Unstaged,
+                        staged_text.await.map(DiffBasesChange::SetIndex),
+                        buffer,
+                        cx,
+                    )
+                    .await
+                    .map_err(Arc::new)
+                })
+                .shared()
+            })
+            .clone();
+
         cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
@@ -492,32 +597,26 @@ impl GitStore {
             }
         }
 
-        let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {
-            hash_map::Entry::Occupied(e) => e.get().clone(),
-            hash_map::Entry::Vacant(entry) => {
-                let changes = self
-                    .state
-                    .load_committed_text(&buffer, &self.buffer_store, cx);
-
-                entry
-                    .insert(
-                        cx.spawn(async move |this, cx| {
-                            Self::open_diff_internal(
-                                this,
-                                DiffKind::Uncommitted,
-                                changes.await,
-                                buffer,
-                                cx,
-                            )
-                            .await
-                            .map_err(Arc::new)
-                        })
-                        .shared(),
-                    )
-                    .clone()
-            }
+        let Some((repo, repo_path)) =
+            self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
+        else {
+            return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
         };
 
+        let task = self
+            .loading_diffs
+            .entry((buffer_id, DiffKind::Uncommitted))
+            .or_insert_with(|| {
+                let changes = repo.read(cx).load_committed_text(buffer_id, repo_path, cx);
+                cx.spawn(async move |this, cx| {
+                    Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx)
+                        .await
+                        .map_err(Arc::new)
+                })
+                .shared()
+            })
+            .clone();
+
         cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
@@ -607,12 +706,7 @@ impl GitStore {
         cx: &App,
     ) -> Option<FileStatus> {
         let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
-        Some(
-            repo.read(cx)
-                .repository_entry
-                .status_for_path(&repo_path)?
-                .status,
-        )
+        Some(repo.read(cx).status_for_path(&repo_path)?.status)
     }
 
     pub fn checkpoint(&self, cx: &App) -> Task<Result<GitStoreCheckpoint>> {
@@ -620,8 +714,7 @@ impl GitStore {
         let mut checkpoints = Vec::new();
         for repository in self.repositories.values() {
             let repository = repository.read(cx);
-            work_directory_abs_paths
-                .push(repository.repository_entry.work_directory_abs_path.clone());
+            work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone());
             checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
         }
 
@@ -640,15 +733,7 @@ impl GitStore {
         let repositories_by_work_dir_abs_path = self
             .repositories
             .values()
-            .map(|repo| {
-                (
-                    repo.read(cx)
-                        .repository_entry
-                        .work_directory_abs_path
-                        .clone(),
-                    repo,
-                )
-            })
+            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
             .collect::<HashMap<_, _>>();
 
         let mut tasks = Vec::new();
@@ -674,15 +759,7 @@ impl GitStore {
         let repositories_by_work_dir_abs_path = self
             .repositories
             .values()
-            .map(|repo| {
-                (
-                    repo.read(cx)
-                        .repository_entry
-                        .work_directory_abs_path
-                        .clone(),
-                    repo,
-                )
-            })
+            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
             .collect::<HashMap<_, _>>();
 
         let mut tasks = Vec::new();
@@ -714,15 +791,7 @@ impl GitStore {
         let repositories_by_work_directory_abs_path = self
             .repositories
             .values()
-            .map(|repo| {
-                (
-                    repo.read(cx)
-                        .repository_entry
-                        .work_directory_abs_path
-                        .clone(),
-                    repo,
-                )
-            })
+            .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
             .collect::<HashMap<_, _>>();
 
         let mut tasks = Vec::new();
@@ -748,60 +817,39 @@ impl GitStore {
         cx: &App,
     ) -> Task<Result<Option<Blame>>> {
         let buffer = buffer.read(cx);
-        let Some(file) = File::from_dyn(buffer.file()) else {
-            return Task::ready(Err(anyhow!("buffer has no file")));
+        let Some((repo, repo_path)) =
+            self.repository_and_path_for_buffer_id(buffer.remote_id(), cx)
+        else {
+            return Task::ready(Err(anyhow!("failed to find a git repository for buffer")));
         };
-
-        match file.worktree.clone().read(cx) {
-            Worktree::Local(worktree) => {
-                let worktree = worktree.snapshot();
-                let blame_params = maybe!({
-                    let local_repo = match worktree.local_repo_containing_path(&file.path) {
-                        Some(repo_for_path) => repo_for_path,
-                        None => return Ok(None),
-                    };
-
-                    let relative_path = local_repo
-                        .relativize(&file.path)
-                        .context("failed to relativize buffer path")?;
-
-                    let repo = local_repo.repo().clone();
-
-                    let content = match version {
-                        Some(version) => buffer.rope_for_version(&version).clone(),
-                        None => buffer.as_rope().clone(),
-                    };
-
-                    anyhow::Ok(Some((repo, relative_path, content)))
-                });
-
-                cx.spawn(async move |_cx| {
-                    let Some((repo, relative_path, content)) = blame_params? else {
-                        return Ok(None);
-                    };
-                    repo.blame(relative_path.clone(), content)
-                        .await
-                        .with_context(|| format!("Failed to blame {:?}", relative_path.0))
-                        .map(Some)
-                })
-            }
-            Worktree::Remote(worktree) => {
-                let buffer_id = buffer.remote_id();
-                let version = buffer.version();
-                let project_id = worktree.project_id();
-                let client = worktree.client();
-                cx.spawn(async move |_| {
+        let content = match &version {
+            Some(version) => buffer.rope_for_version(version).clone(),
+            None => buffer.as_rope().clone(),
+        };
+        let version = version.unwrap_or(buffer.version());
+        let buffer_id = buffer.remote_id();
+
+        let rx = repo.read(cx).send_job(move |state, _| async move {
+            match state {
+                RepositoryState::Local { backend, .. } => backend
+                    .blame(repo_path.clone(), content)
+                    .await
+                    .with_context(|| format!("Failed to blame {:?}", repo_path.0))
+                    .map(Some),
+                RepositoryState::Remote { project_id, client } => {
                     let response = client
                         .request(proto::BlameBuffer {
-                            project_id,
+                            project_id: project_id.to_proto(),
                             buffer_id: buffer_id.into(),
                             version: serialize_version(&version),
                         })
                         .await?;
                     Ok(deserialize_blame_buffer_response(response))
-                })
+                }
             }
-        }
+        });
+
+        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
     }
 
     pub fn get_permalink_to_line(
@@ -810,64 +858,53 @@ impl GitStore {
         selection: Range<u32>,
         cx: &App,
     ) -> Task<Result<url::Url>> {
-        let buffer = buffer.read(cx);
-        let Some(file) = File::from_dyn(buffer.file()) else {
+        let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
             return Task::ready(Err(anyhow!("buffer has no file")));
         };
 
-        match file.worktree.read(cx) {
-            Worktree::Local(worktree) => {
-                let repository = self
-                    .repository_and_path_for_project_path(
-                        &(worktree.id(), file.path.clone()).into(),
-                        cx,
-                    )
-                    .map(|(repository, _)| repository);
-                let Some((local_repo_entry, repo_entry)) = repository.and_then(|repository| {
-                    let repository = repository.read(cx);
-                    let repo_entry = repository.repository_entry.clone();
-                    Some((worktree.get_local_repo(&repo_entry)?, repo_entry))
-                }) else {
-                    // If we're not in a Git repo, check whether this is a Rust source
-                    // file in the Cargo registry (presumably opened with go-to-definition
-                    // from a normal Rust file). If so, we can put together a permalink
-                    // using crate metadata.
-                    if buffer
-                        .language()
-                        .is_none_or(|lang| lang.name() != "Rust".into())
-                    {
-                        return Task::ready(Err(anyhow!("no permalink available")));
-                    }
-                    let Some(file_path) = worktree.absolutize(&file.path).ok() else {
-                        return Task::ready(Err(anyhow!("no permalink available")));
-                    };
-                    return cx.spawn(async move |cx| {
-                        let provider_registry =
-                            cx.update(GitHostingProviderRegistry::default_global)?;
-                        get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
-                            .map_err(|_| anyhow!("no permalink available"))
-                    });
-                };
-
-                let path = match local_repo_entry.relativize(&file.path) {
-                    Ok(RepoPath(path)) => path,
-                    Err(e) => return Task::ready(Err(e)),
-                };
+        let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
+            &(file.worktree.read(cx).id(), file.path.clone()).into(),
+            cx,
+        ) else {
+            // If we're not in a Git repo, check whether this is a Rust source
+            // file in the Cargo registry (presumably opened with go-to-definition
+            // from a normal Rust file). If so, we can put together a permalink
+            // using crate metadata.
+            if buffer
+                .read(cx)
+                .language()
+                .is_none_or(|lang| lang.name() != "Rust".into())
+            {
+                return Task::ready(Err(anyhow!("no permalink available")));
+            }
+            let Some(file_path) = file.worktree.read(cx).absolutize(&file.path).ok() else {
+                return Task::ready(Err(anyhow!("no permalink available")));
+            };
+            return cx.spawn(async move |cx| {
+                let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?;
+                get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
+                    .map_err(|_| anyhow!("no permalink available"))
+            });
 
-                let remote = repo_entry
-                    .branch()
-                    .and_then(|b| b.upstream.as_ref())
-                    .and_then(|b| b.remote_name())
-                    .unwrap_or("origin")
-                    .to_string();
+            // TODO remote case
+        };
 
-                let repo = local_repo_entry.repo().clone();
-                cx.spawn(async move |cx| {
-                    let origin_url = repo
+        let buffer_id = buffer.read(cx).remote_id();
+        let branch = repo.read(cx).branch.clone();
+        let remote = branch
+            .as_ref()
+            .and_then(|b| b.upstream.as_ref())
+            .and_then(|b| b.remote_name())
+            .unwrap_or("origin")
+            .to_string();
+        let rx = repo.read(cx).send_job(move |state, cx| async move {
+            match state {
+                RepositoryState::Local { backend, .. } => {
+                    let origin_url = backend
                         .remote_url(&remote)
                         .ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
 
-                    let sha = repo
+                    let sha = backend
                         .head_sha()
                         .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
 
@@ -878,7 +915,7 @@ impl GitStore {
                         parse_git_remote_url(provider_registry, &origin_url)
                             .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
 
-                    let path = path
+                    let path = repo_path
                         .to_str()
                         .ok_or_else(|| anyhow!("failed to convert path to string"))?;
 
@@ -890,16 +927,11 @@ impl GitStore {
                             selection: Some(selection),
                         },
                     ))
-                })
-            }
-            Worktree::Remote(worktree) => {
-                let buffer_id = buffer.remote_id();
-                let project_id = worktree.project_id();
-                let client = worktree.client();
-                cx.spawn(async move |_| {
+                }
+                RepositoryState::Remote { project_id, client } => {
                     let response = client
                         .request(proto::GetPermalinkToLine {
-                            project_id,
+                            project_id: project_id.to_proto(),
                             buffer_id: buffer_id.into(),
                             selection: Some(proto::Range {
                                 start: selection.start as u64,
@@ -909,20 +941,23 @@ impl GitStore {
                         .await?;
 
                     url::Url::parse(&response.permalink).context("failed to parse permalink")
-                })
+                }
             }
-        }
+        });
+        cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
     }
 
     fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
         match &self.state {
             GitStoreState::Local {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => downstream_client
                 .as_ref()
                 .map(|state| (state.client.clone(), state.project_id)),
             GitStoreState::Ssh {
-                downstream_client, ..
+                downstream: downstream_client,
+                ..
             } => downstream_client.clone(),
             GitStoreState::Remote { .. } => None,
         }
@@ -940,160 +975,148 @@ impl GitStore {
         }
     }
 
-    fn project_environment(&self) -> Option<Entity<ProjectEnvironment>> {
-        match &self.state {
-            GitStoreState::Local { environment, .. } => Some(environment.clone()),
-            GitStoreState::Ssh { environment, .. } => Some(environment.clone()),
-            GitStoreState::Remote { .. } => None,
-        }
-    }
-
-    fn project_id(&self) -> Option<ProjectId> {
-        match &self.state {
-            GitStoreState::Local { .. } => None,
-            GitStoreState::Ssh { .. } => Some(ProjectId(proto::SSH_PROJECT_ID)),
-            GitStoreState::Remote { project_id, .. } => Some(*project_id),
-        }
-    }
-
     fn on_worktree_store_event(
         &mut self,
         worktree_store: Entity<WorktreeStore>,
         event: &WorktreeStoreEvent,
         cx: &mut Context<Self>,
     ) {
+        let GitStoreState::Local {
+            project_environment,
+            downstream,
+            next_repository_id,
+            fs,
+        } = &self.state
+        else {
+            return;
+        };
+
         match event {
-            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
-                // We should only get this event for a local project.
-                self.update_repositories(&worktree_store, cx);
-                if self.is_local() {
-                    if let Some(worktree) =
-                        worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
-                    {
-                        self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
-                    }
+            WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => {
+                let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default();
+                for (relative_path, _, _) in updated_entries.iter() {
+                    let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
+                        &(*worktree_id, relative_path.clone()).into(),
+                        cx,
+                    ) else {
+                        continue;
+                    };
+                    paths_by_git_repo.entry(repo).or_default().push(repo_path)
+                }
+
+                for (repo, paths) in paths_by_git_repo {
+                    repo.update(cx, |repo, cx| {
+                        repo.paths_changed(
+                            paths,
+                            downstream
+                                .as_ref()
+                                .map(|downstream| downstream.updates_tx.clone()),
+                            cx,
+                        );
+                    });
                 }
-                cx.emit(GitEvent::GitStateUpdated);
             }
-            WorktreeStoreEvent::WorktreeAdded(_) => {}
-            _ => {
-                cx.emit(GitEvent::FileSystemUpdated);
+            WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
+                self.update_repositories_from_worktrees(
+                    project_environment.clone(),
+                    next_repository_id.clone(),
+                    downstream
+                        .as_ref()
+                        .map(|downstream| downstream.updates_tx.clone()),
+                    changed_repos.clone(),
+                    fs.clone(),
+                    cx,
+                );
+                if let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx) {
+                    self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
+                }
             }
+            _ => {}
         }
     }
 
-    fn update_repositories(
+    fn on_repository_event(
         &mut self,
-        worktree_store: &Entity<WorktreeStore>,
-        cx: &mut Context<GitStore>,
+        repo: Entity<Repository>,
+        event: &RepositoryEvent,
+        cx: &mut Context<Self>,
     ) {
-        let mut new_repositories = HashMap::default();
-        let git_store = cx.weak_entity();
-        worktree_store.update(cx, |worktree_store, cx| {
-            for worktree in worktree_store.worktrees() {
-                worktree.update(cx, |worktree, cx| {
-                    let snapshot = worktree.snapshot();
-                    for repo_entry in snapshot.repositories().iter() {
-                        let git_repo_and_merge_message = worktree
-                            .as_local()
-                            .and_then(|local_worktree| local_worktree.get_local_repo(repo_entry))
-                            .map(|local_repo| {
-                                (
-                                    RepositoryState::Local(local_repo.repo().clone()),
-                                    local_repo.merge_message.clone(),
-                                )
-                            })
-                            .or_else(|| {
-                                let git_repo = RepositoryState::Remote {
-                                    project_id: self.project_id()?,
-                                    client: self
-                                        .upstream_client()
-                                        .context("no upstream client")
-                                        .log_err()?
-                                        .clone(),
-                                    work_directory_id: repo_entry.work_directory_id(),
-                                };
-                                Some((git_repo, None))
-                            });
-
-                        let Some((git_repo, merge_message)) = git_repo_and_merge_message else {
-                            continue;
-                        };
-
-                        let existing_repo = self
-                            .repositories
-                            .values()
-                            .find(|repo| repo.read(cx).id() == repo_entry.work_directory_id());
-
-                        let repo = if let Some(existing_repo) = existing_repo {
-                            // Update the statuses and merge message but keep everything else.
-                            let existing_repo = existing_repo.clone();
-                            existing_repo.update(cx, |existing_repo, _| {
-                                existing_repo.repository_entry = repo_entry.clone();
-                                if matches!(git_repo, RepositoryState::Local { .. }) {
-                                    existing_repo.merge_message = merge_message;
-                                    existing_repo.completed_scan_id = worktree.completed_scan_id();
-                                }
-                            });
-                            existing_repo
-                        } else {
-                            cx.new(|_| Repository {
-                                worktree_id: Some(worktree.id()),
-                                project_environment: self
-                                    .project_environment()
-                                    .as_ref()
-                                    .map(|env| env.downgrade()),
-                                git_store: git_store.clone(),
-                                askpass_delegates: Default::default(),
-                                latest_askpass_id: 0,
-                                repository_entry: repo_entry.clone(),
-                                job_sender: self.update_sender.clone(),
-                                merge_message,
-                                commit_message_buffer: None,
-                                completed_scan_id: worktree.completed_scan_id(),
-                                state: git_repo,
-                            })
-                        };
-
-                        // TODO only send out messages for repository snapshots that have changed
-                        let snapshot = repo.read(cx).repository_entry.clone();
-                        if let GitStoreState::Local {
-                            downstream_client: Some(state),
-                            ..
-                        } = &self.state
-                        {
-                            state
-                                .updates_tx
-                                .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
-                                .ok();
-                        }
-                        new_repositories.insert(repo_entry.work_directory_id(), repo);
-                        self.repositories.remove(&repo_entry.work_directory_id());
-                    }
-                })
-            }
-        });
+        let id = repo.read(cx).id;
+        cx.emit(GitStoreEvent::RepositoryUpdated(
+            id,
+            event.clone(),
+            self.active_repo_id == Some(id),
+        ))
+    }
 
-        if let GitStoreState::Local {
-            downstream_client: Some(state),
-            ..
-        } = &self.state
-        {
-            for id in self.repositories.keys().cloned() {
-                state
-                    .updates_tx
-                    .unbounded_send(DownstreamUpdate::RemoveRepository(id))
-                    .ok();
+    /// Update our list of repositories and schedule git scans in response to a notification from a worktree,
+    fn update_repositories_from_worktrees(
+        &mut self,
+        project_environment: Entity<ProjectEnvironment>,
+        next_repository_id: Arc<AtomicU64>,
+        updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
+        updated_git_repositories: UpdatedGitRepositoriesSet,
+        fs: Arc<dyn Fs>,
+        cx: &mut Context<Self>,
+    ) {
+        let mut removed_ids = Vec::new();
+        for update in updated_git_repositories.iter() {
+            if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| {
+                Some(&repo.read(cx).work_directory_abs_path)
+                    == update.old_work_directory_abs_path.as_ref()
+            }) {
+                if let Some(new_work_directory_abs_path) =
+                    update.new_work_directory_abs_path.clone()
+                {
+                    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);
+                }
+            } else if let Some((work_directory_abs_path, dot_git_abs_path)) = update
+                .new_work_directory_abs_path
+                .clone()
+                .zip(update.dot_git_abs_path.clone())
+            {
+                let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
+                let git_store = cx.weak_entity();
+                let repo = cx.new(|cx| {
+                    let mut repo = Repository::local(
+                        id,
+                        work_directory_abs_path,
+                        dot_git_abs_path,
+                        project_environment.downgrade(),
+                        fs.clone(),
+                        git_store,
+                        cx,
+                    );
+                    repo.schedule_scan(updates_tx.clone(), cx);
+                    repo
+                });
+                self._subscriptions
+                    .push(cx.subscribe(&repo, Self::on_repository_event));
+                self.repositories.insert(id, repo);
+                cx.emit(GitStoreEvent::RepositoryAdded(id));
+                self.active_repo_id.get_or_insert_with(|| {
+                    cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
+                    id
+                });
             }
         }
 
-        self.repositories = new_repositories;
-        if let Some(id) = self.active_repo_id.as_ref() {
-            if !self.repositories.contains_key(id) {
+        for id in removed_ids {
+            if self.active_repo_id == Some(id) {
                 self.active_repo_id = None;
+                cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
+            }
+            self.repositories.remove(&id);
+            if let Some(updates_tx) = updates_tx.as_ref() {
+                updates_tx
+                    .unbounded_send(DownstreamUpdate::RemoveRepository(id))
+                    .ok();
             }
-        } else if let Some(&first_id) = self.repositories.keys().next() {
-            self.active_repo_id = Some(first_id);
         }
     }
 

crates/project/src/git_store/git_traversal.rs 🔗

@@ -3,21 +3,21 @@ use git::status::GitSummary;
 use std::{ops::Deref, path::Path};
 use sum_tree::Cursor;
 use text::Bias;
-use worktree::{
-    Entry, PathProgress, PathTarget, ProjectEntryId, RepositoryEntry, StatusEntry, Traversal,
-};
+use worktree::{Entry, PathProgress, PathTarget, Traversal};
+
+use super::{RepositoryId, RepositorySnapshot, StatusEntry};
 
 /// Walks the worktree entries and their associated git statuses.
 pub struct GitTraversal<'a> {
     traversal: Traversal<'a>,
     current_entry_summary: Option<GitSummary>,
-    repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
-    repo_location: Option<(ProjectEntryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
+    repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
+    repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
 }
 
 impl<'a> GitTraversal<'a> {
     pub fn new(
-        repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
+        repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
         traversal: Traversal<'a>,
     ) -> GitTraversal<'a> {
         let mut this = GitTraversal {
@@ -46,8 +46,8 @@ impl<'a> GitTraversal<'a> {
             .repo_snapshots
             .values()
             .filter_map(|repo_snapshot| {
-                let relative_path = repo_snapshot.relativize_abs_path(&abs_path)?;
-                Some((repo_snapshot, relative_path))
+                let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?;
+                Some((repo_snapshot, repo_path))
             })
             .max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
         else {
@@ -61,12 +61,9 @@ impl<'a> GitTraversal<'a> {
                 .repo_location
                 .as_ref()
                 .map(|(prev_repo_id, _)| *prev_repo_id)
-                != Some(repo.work_directory_id())
+                != Some(repo.id)
         {
-            self.repo_location = Some((
-                repo.work_directory_id(),
-                repo.statuses_by_path.cursor::<PathProgress>(&()),
-            ));
+            self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::<PathProgress>(&())));
         }
 
         let Some((_, statuses)) = &mut self.repo_location else {
@@ -148,7 +145,7 @@ pub struct ChildEntriesGitIter<'a> {
 
 impl<'a> ChildEntriesGitIter<'a> {
     pub fn new(
-        repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
+        repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
         worktree_snapshot: &'a worktree::Snapshot,
         parent_path: &'a Path,
     ) -> Self {
@@ -771,7 +768,7 @@ mod tests {
 
     #[track_caller]
     fn check_git_statuses(
-        repo_snapshots: &HashMap<ProjectEntryId, RepositoryEntry>,
+        repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
         worktree_snapshot: &worktree::Snapshot,
         expected_statuses: &[(&Path, GitSummary)],
     ) {

crates/project/src/lsp_store.rs 🔗

@@ -8078,9 +8078,7 @@ impl LspStore {
         });
 
         if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
-            environment.update(cx, |env, cx| {
-                env.get_environment(worktree_id, worktree_abs_path, cx)
-            })
+            environment.update(cx, |env, cx| env.get_environment(worktree_abs_path, cx))
         } else {
             Task::ready(None).shared()
         }
@@ -9864,13 +9862,10 @@ impl LocalLspAdapterDelegate {
         fs: Arc<dyn Fs>,
         cx: &mut App,
     ) -> Arc<Self> {
-        let (worktree_id, worktree_abs_path) = {
-            let worktree = worktree.read(cx);
-            (worktree.id(), worktree.abs_path())
-        };
+        let worktree_abs_path = worktree.read(cx).abs_path();
 
         let load_shell_env_task = environment.update(cx, |env, cx| {
-            env.get_environment(Some(worktree_id), Some(worktree_abs_path), cx)
+            env.get_environment(Some(worktree_abs_path), cx)
         });
 
         Arc::new(Self {

crates/project/src/project.rs 🔗

@@ -24,7 +24,7 @@ mod direnv;
 mod environment;
 use buffer_diff::BufferDiff;
 pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
-use git_store::{GitEvent, Repository};
+use git_store::{Repository, RepositoryId};
 pub mod search_history;
 mod yarn;
 
@@ -300,8 +300,6 @@ pub enum Event {
     RevealInProjectPanel(ProjectEntryId),
     SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
     ExpandedAllForEntry(WorktreeId, ProjectEntryId),
-    GitStateUpdated,
-    ActiveRepositoryChanged,
 }
 
 pub enum DebugAdapterClientState {
@@ -924,7 +922,6 @@ impl Project {
                     cx,
                 )
             });
-            cx.subscribe(&git_store, Self::on_git_store_event).detach();
 
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 
@@ -1064,13 +1061,7 @@ impl Project {
             });
 
             let git_store = cx.new(|cx| {
-                GitStore::ssh(
-                    &worktree_store,
-                    buffer_store.clone(),
-                    environment.clone(),
-                    ssh_proto.clone(),
-                    cx,
-                )
+                GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
             });
 
             cx.subscribe(&ssh, Self::on_ssh_event).detach();
@@ -1655,13 +1646,13 @@ impl Project {
     pub fn shell_environment_errors<'a>(
         &'a self,
         cx: &'a App,
-    ) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
+    ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
         self.environment.read(cx).environment_errors()
     }
 
-    pub fn remove_environment_error(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) {
+    pub fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
         self.environment.update(cx, |environment, cx| {
-            environment.remove_environment_error(worktree_id, cx);
+            environment.remove_environment_error(abs_path, cx);
         });
     }
 
@@ -2760,19 +2751,6 @@ 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>,
@@ -4794,7 +4772,7 @@ impl Project {
         self.git_store.read(cx).active_repository()
     }
 
-    pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<ProjectEntryId, Entity<Repository>> {
+    pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<RepositoryId, Entity<Repository>> {
         self.git_store.read(cx).repositories()
     }
 

crates/project/src/project_tests.rs 🔗

@@ -1,12 +1,19 @@
 #![allow(clippy::format_collect)]
 
-use crate::{Event, task_inventory::TaskContexts, task_store::TaskSettingsLocation, *};
+use crate::{
+    Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
+    *,
+};
 use buffer_diff::{
     BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,
 };
 use fs::FakeFs;
 use futures::{StreamExt, future};
-use git::repository::RepoPath;
+use git::{
+    repository::RepoPath,
+    status::{StatusCode, TrackedStatus},
+};
+use git2::RepositoryInitOptions;
 use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
 use http_client::Url;
 use language::{
@@ -21,6 +28,7 @@ use lsp::{
 };
 use parking_lot::Mutex;
 use paths::tasks_file;
+use postage::stream::Stream as _;
 use pretty_assertions::{assert_eq, assert_matches};
 use serde_json::json;
 #[cfg(not(windows))]
@@ -7067,7 +7075,7 @@ async fn test_repository_and_path_for_project_path(
                 (
                     path,
                     result.map(|(repo, repo_path)| {
-                        (Path::new(repo).to_owned(), RepoPath::from(repo_path))
+                        (Path::new(repo).into(), RepoPath::from(repo_path))
                     }),
                 )
             })
@@ -7079,13 +7087,7 @@ async fn test_repository_and_path_for_project_path(
                 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,
-                    ))
+                    Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
                 });
                 (path, result)
             })
@@ -7160,13 +7162,830 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
                 .unwrap()
                 .0
                 .read(cx)
-                .repository_entry
-                .work_directory_abs_path,
+                .work_directory_abs_path
+                .as_ref(),
             Path::new(path!("/root/home"))
         );
     });
 }
 
+#[gpui::test]
+async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+
+    let root = TempTree::new(json!({
+        "project": {
+            "a.txt": "a",    // Modified
+            "b.txt": "bb",   // Added
+            "c.txt": "ccc",  // Unchanged
+            "d.txt": "dddd", // Deleted
+        },
+    }));
+
+    // Set up git repository before creating the project.
+    let work_dir = root.path().join("project");
+    let repo = git_init(work_dir.as_path());
+    git_add("a.txt", &repo);
+    git_add("c.txt", &repo);
+    git_add("d.txt", &repo);
+    git_commit("Initial commit", &repo);
+    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
+    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
+
+    let project = Project::test(
+        Arc::new(RealFs::new(None, cx.executor())),
+        [root.path()],
+        cx,
+    )
+    .await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    // Check that the right git state is observed on startup
+    repository.read_with(cx, |repository, _| {
+        let entries = repository.cached_status().collect::<Vec<_>>();
+        assert_eq!(
+            entries,
+            [
+                StatusEntry {
+                    repo_path: "a.txt".into(),
+                    status: StatusCode::Modified.worktree(),
+                },
+                StatusEntry {
+                    repo_path: "b.txt".into(),
+                    status: FileStatus::Untracked,
+                },
+                StatusEntry {
+                    repo_path: "d.txt".into(),
+                    status: StatusCode::Deleted.worktree(),
+                },
+            ]
+        );
+    });
+
+    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
+
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    repository.read_with(cx, |repository, _| {
+        let entries = repository.cached_status().collect::<Vec<_>>();
+        assert_eq!(
+            entries,
+            [
+                StatusEntry {
+                    repo_path: "a.txt".into(),
+                    status: StatusCode::Modified.worktree(),
+                },
+                StatusEntry {
+                    repo_path: "b.txt".into(),
+                    status: FileStatus::Untracked,
+                },
+                StatusEntry {
+                    repo_path: "c.txt".into(),
+                    status: StatusCode::Modified.worktree(),
+                },
+                StatusEntry {
+                    repo_path: "d.txt".into(),
+                    status: StatusCode::Deleted.worktree(),
+                },
+            ]
+        );
+    });
+
+    git_add("a.txt", &repo);
+    git_add("c.txt", &repo);
+    git_remove_index(Path::new("d.txt"), &repo);
+    git_commit("Another commit", &repo);
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
+    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    repository.read_with(cx, |repository, _cx| {
+        let entries = repository.cached_status().collect::<Vec<_>>();
+
+        // Deleting an untracked entry, b.txt, should leave no status
+        // a.txt was tracked, and so should have a status
+        assert_eq!(
+            entries,
+            [StatusEntry {
+                repo_path: "a.txt".into(),
+                status: StatusCode::Deleted.worktree(),
+            }]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+
+    let root = TempTree::new(json!({
+        "project": {
+            "sub": {},
+            "a.txt": "",
+        },
+    }));
+
+    let work_dir = root.path().join("project");
+    let repo = git_init(work_dir.as_path());
+    // a.txt exists in HEAD and the working copy but is deleted in the index.
+    git_add("a.txt", &repo);
+    git_commit("Initial commit", &repo);
+    git_remove_index("a.txt".as_ref(), &repo);
+    // `sub` is a nested git repository.
+    let _sub = git_init(&work_dir.join("sub"));
+
+    let project = Project::test(
+        Arc::new(RealFs::new(None, cx.executor())),
+        [root.path()],
+        cx,
+    )
+    .await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project
+            .repositories(cx)
+            .values()
+            .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
+            .unwrap()
+            .clone()
+    });
+
+    repository.read_with(cx, |repository, _cx| {
+        let entries = repository.cached_status().collect::<Vec<_>>();
+
+        // `sub` doesn't appear in our computed statuses.
+        // a.txt appears with a combined `DA` status.
+        assert_eq!(
+            entries,
+            [StatusEntry {
+                repo_path: "a.txt".into(),
+                status: TrackedStatus {
+                    index_status: StatusCode::Deleted,
+                    worktree_status: StatusCode::Added
+                }
+                .into(),
+            }]
+        )
+    });
+}
+
+#[gpui::test]
+async fn test_repository_subfolder_git_status(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+
+    let root = TempTree::new(json!({
+        "my-repo": {
+            // .git folder will go here
+            "a.txt": "a",
+            "sub-folder-1": {
+                "sub-folder-2": {
+                    "c.txt": "cc",
+                    "d": {
+                        "e.txt": "eee"
+                    }
+                },
+            }
+        },
+    }));
+
+    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
+    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
+
+    // Set up git repository before creating the worktree.
+    let git_repo_work_dir = root.path().join("my-repo");
+    let repo = git_init(git_repo_work_dir.as_path());
+    git_add(C_TXT, &repo);
+    git_commit("Initial commit", &repo);
+
+    // Open the worktree in subfolder
+    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
+
+    let project = Project::test(
+        Arc::new(RealFs::new(None, cx.executor())),
+        [root.path().join(project_root).as_path()],
+        cx,
+    )
+    .await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    // Ensure that the git status is loaded correctly
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(
+            repository.work_directory_abs_path.canonicalize().unwrap(),
+            root.path().join("my-repo").canonicalize().unwrap()
+        );
+
+        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
+        assert_eq!(
+            repository.status_for_path(&E_TXT.into()).unwrap().status,
+            FileStatus::Untracked
+        );
+    });
+
+    // Now we simulate FS events, but ONLY in the .git folder that's outside
+    // of out project root.
+    // Meaning: we don't produce any FS events for files inside the project.
+    git_add(E_TXT, &repo);
+    git_commit("Second commit", &repo);
+    tree.flush_fs_events_in_root_git_repository(cx).await;
+    cx.executor().run_until_parked();
+
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(repository.status_for_path(&C_TXT.into()), None);
+        assert_eq!(repository.status_for_path(&E_TXT.into()), None);
+    });
+}
+
+// TODO: this test fails on Windows because upon cherry-picking we don't get an event in the .git directory,
+// despite CHERRY_PICK_HEAD existing after the `git_cherry_pick` call and the conflicted path showing up in git status.
+#[cfg(not(windows))]
+#[gpui::test]
+async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+
+    let root = TempTree::new(json!({
+        "project": {
+            "a.txt": "a",
+        },
+    }));
+    let root_path = root.path();
+
+    let repo = git_init(&root_path.join("project"));
+    git_add("a.txt", &repo);
+    git_commit("init", &repo);
+
+    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    git_branch("other-branch", &repo);
+    git_checkout("refs/heads/other-branch", &repo);
+    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
+    git_add("a.txt", &repo);
+    git_commit("capitalize", &repo);
+    let commit = repo
+        .head()
+        .expect("Failed to get HEAD")
+        .peel_to_commit()
+        .expect("HEAD is not a commit");
+    git_checkout("refs/heads/main", &repo);
+    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
+    git_add("a.txt", &repo);
+    git_commit("improve letter", &repo);
+    git_cherry_pick(&commit, &repo);
+    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
+        .expect("No CHERRY_PICK_HEAD");
+    pretty_assertions::assert_eq!(
+        git_status(&repo),
+        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
+    );
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+    let conflicts = repository.update(cx, |repository, _| {
+        repository
+            .merge_conflicts
+            .iter()
+            .cloned()
+            .collect::<Vec<_>>()
+    });
+    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
+
+    git_add("a.txt", &repo);
+    // Attempt to manually simulate what `git cherry-pick --continue` would do.
+    git_commit("whatevs", &repo);
+    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
+        .expect("Failed to remove CHERRY_PICK_HEAD");
+    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
+    tree.flush_fs_events(cx).await;
+    let conflicts = repository.update(cx, |repository, _| {
+        repository
+            .merge_conflicts
+            .iter()
+            .cloned()
+            .collect::<Vec<_>>()
+    });
+    pretty_assertions::assert_eq!(conflicts, []);
+}
+
+#[gpui::test]
+async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        path!("/root"),
+        json!({
+            ".git": {},
+            ".gitignore": "*.txt\n",
+            "a.xml": "<a></a>",
+            "b.txt": "Some text"
+        }),
+    )
+    .await;
+
+    fs.set_head_and_index_for_repo(
+        path!("/root/.git").as_ref(),
+        &[
+            (".gitignore".into(), "*.txt\n".into()),
+            ("a.xml".into(), "<a></a>".into()),
+        ],
+    );
+
+    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());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    // One file is unmodified, the other is ignored.
+    cx.read(|cx| {
+        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
+        assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
+    });
+
+    // Change the gitignore, and stage the newly non-ignored file.
+    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
+        .await
+        .unwrap();
+    fs.set_index_for_repo(
+        Path::new(path!("/root/.git")),
+        &[
+            (".gitignore".into(), "*.txt\n".into()),
+            ("a.xml".into(), "<a></a>".into()),
+            ("b.txt".into(), "Some text".into()),
+        ],
+    );
+
+    cx.executor().run_until_parked();
+    cx.read(|cx| {
+        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "b.txt",
+            Some(StatusCode::Added),
+            false,
+        );
+    });
+}
+
+// NOTE:
+// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
+// a directory which some program has already open.
+// This is a limitation of the Windows.
+// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+#[gpui::test]
+#[cfg_attr(target_os = "windows", ignore)]
+async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+    let root = TempTree::new(json!({
+        "projects": {
+            "project1": {
+                "a": "",
+                "b": "",
+            }
+        },
+
+    }));
+    let root_path = root.path();
+
+    let repo = git_init(&root_path.join("projects/project1"));
+    git_add("a", &repo);
+    git_commit("init", &repo);
+    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
+
+    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    repository.read_with(cx, |repository, _| {
+        assert_eq!(
+            repository.work_directory_abs_path.as_ref(),
+            root_path.join("projects/project1").as_path()
+        );
+        assert_eq!(
+            repository
+                .status_for_path(&"a".into())
+                .map(|entry| entry.status),
+            Some(StatusCode::Modified.worktree()),
+        );
+        assert_eq!(
+            repository
+                .status_for_path(&"b".into())
+                .map(|entry| entry.status),
+            Some(FileStatus::Untracked),
+        );
+    });
+
+    std::fs::rename(
+        root_path.join("projects/project1"),
+        root_path.join("projects/project2"),
+    )
+    .unwrap();
+    tree.flush_fs_events(cx).await;
+
+    repository.read_with(cx, |repository, _| {
+        assert_eq!(
+            repository.work_directory_abs_path.as_ref(),
+            root_path.join("projects/project2").as_path()
+        );
+        assert_eq!(
+            repository.status_for_path(&"a".into()).unwrap().status,
+            StatusCode::Modified.worktree(),
+        );
+        assert_eq!(
+            repository.status_for_path(&"b".into()).unwrap().status,
+            FileStatus::Untracked,
+        );
+    });
+}
+
+// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
+// you can't rename a directory which some program has already open. This is a
+// limitation of the Windows. See:
+// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
+#[gpui::test]
+#[cfg_attr(target_os = "windows", ignore)]
+async fn test_file_status(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.executor().allow_parking();
+    const IGNORE_RULE: &str = "**/target";
+
+    let root = TempTree::new(json!({
+        "project": {
+            "a.txt": "a",
+            "b.txt": "bb",
+            "c": {
+                "d": {
+                    "e.txt": "eee"
+                }
+            },
+            "f.txt": "ffff",
+            "target": {
+                "build_file": "???"
+            },
+            ".gitignore": IGNORE_RULE
+        },
+
+    }));
+    let root_path = root.path();
+
+    const A_TXT: &str = "a.txt";
+    const B_TXT: &str = "b.txt";
+    const E_TXT: &str = "c/d/e.txt";
+    const F_TXT: &str = "f.txt";
+    const DOTGITIGNORE: &str = ".gitignore";
+    const BUILD_FILE: &str = "target/build_file";
+
+    // Set up git repository before creating the worktree.
+    let work_dir = root.path().join("project");
+    let mut repo = git_init(work_dir.as_path());
+    repo.add_ignore_rule(IGNORE_RULE).unwrap();
+    git_add(A_TXT, &repo);
+    git_add(E_TXT, &repo);
+    git_add(DOTGITIGNORE, &repo);
+    git_commit("Initial commit", &repo);
+
+    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    // Check that the right git state is observed on startup
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(
+            repository.work_directory_abs_path.as_ref(),
+            root_path.join("project").as_path()
+        );
+
+        assert_eq!(
+            repository.status_for_path(&B_TXT.into()).unwrap().status,
+            FileStatus::Untracked,
+        );
+        assert_eq!(
+            repository.status_for_path(&F_TXT.into()).unwrap().status,
+            FileStatus::Untracked,
+        );
+    });
+
+    // Modify a file in the working copy.
+    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    // The worktree detects that the file's git status has changed.
+    repository.read_with(cx, |repository, _| {
+        assert_eq!(
+            repository.status_for_path(&A_TXT.into()).unwrap().status,
+            StatusCode::Modified.worktree(),
+        );
+    });
+
+    // Create a commit in the git repository.
+    git_add(A_TXT, &repo);
+    git_add(B_TXT, &repo);
+    git_commit("Committing modified and added", &repo);
+    tree.flush_fs_events(cx).await;
+    cx.executor().run_until_parked();
+
+    // The worktree detects that the files' git status have changed.
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(
+            repository.status_for_path(&F_TXT.into()).unwrap().status,
+            FileStatus::Untracked,
+        );
+        assert_eq!(repository.status_for_path(&B_TXT.into()), None);
+        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
+    });
+
+    // Modify files in the working copy and perform git operations on other files.
+    git_reset(0, &repo);
+    git_remove_index(Path::new(B_TXT), &repo);
+    git_stash(&mut repo);
+    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
+    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
+    tree.flush_fs_events(cx).await;
+    cx.executor().run_until_parked();
+
+    // Check that more complex repo changes are tracked
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(repository.status_for_path(&A_TXT.into()), None);
+        assert_eq!(
+            repository.status_for_path(&B_TXT.into()).unwrap().status,
+            FileStatus::Untracked,
+        );
+        assert_eq!(
+            repository.status_for_path(&E_TXT.into()).unwrap().status,
+            StatusCode::Modified.worktree(),
+        );
+    });
+
+    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
+    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
+    std::fs::write(
+        work_dir.join(DOTGITIGNORE),
+        [IGNORE_RULE, "f.txt"].join("\n"),
+    )
+    .unwrap();
+
+    git_add(Path::new(DOTGITIGNORE), &repo);
+    git_commit("Committing modified git ignore", &repo);
+
+    tree.flush_fs_events(cx).await;
+    cx.executor().run_until_parked();
+
+    let mut renamed_dir_name = "first_directory/second_directory";
+    const RENAMED_FILE: &str = "rf.txt";
+
+    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
+    std::fs::write(
+        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
+        "new-contents",
+    )
+    .unwrap();
+
+    tree.flush_fs_events(cx).await;
+    cx.executor().run_until_parked();
+
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(
+            repository
+                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
+                .unwrap()
+                .status,
+            FileStatus::Untracked,
+        );
+    });
+
+    renamed_dir_name = "new_first_directory/second_directory";
+
+    std::fs::rename(
+        work_dir.join("first_directory"),
+        work_dir.join("new_first_directory"),
+    )
+    .unwrap();
+
+    tree.flush_fs_events(cx).await;
+    cx.executor().run_until_parked();
+
+    repository.read_with(cx, |repository, _cx| {
+        assert_eq!(
+            repository
+                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
+                .unwrap()
+                .status,
+            FileStatus::Untracked,
+        );
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
+                project_settings.file_scan_exclusions = Some(Vec::new());
+            });
+        });
+    });
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        path!("/root"),
+        json!({
+            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
+            "tree": {
+                ".git": {},
+                ".gitignore": "ignored-dir\n",
+                "tracked-dir": {
+                    "tracked-file1": "",
+                    "ancestor-ignored-file1": "",
+                },
+                "ignored-dir": {
+                    "ignored-file1": ""
+                }
+            }
+        }),
+    )
+    .await;
+    fs.set_head_and_index_for_repo(
+        path!("/root/tree/.git").as_ref(),
+        &[
+            (".gitignore".into(), "ignored-dir\n".into()),
+            ("tracked-dir/tracked-file1".into(), "".into()),
+        ],
+    );
+
+    let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
+
+    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+    tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+    cx.executor().run_until_parked();
+
+    let repository = project.read_with(cx, |project, cx| {
+        project.repositories(cx).values().next().unwrap().clone()
+    });
+
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
+    })
+    .recv()
+    .await;
+
+    cx.read(|cx| {
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "tracked-dir/tracked-file1",
+            None,
+            false,
+        );
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "tracked-dir/ancestor-ignored-file1",
+            None,
+            false,
+        );
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "ignored-dir/ignored-file1",
+            None,
+            true,
+        );
+    });
+
+    fs.create_file(
+        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    fs.set_index_for_repo(
+        path!("/root/tree/.git").as_ref(),
+        &[
+            (".gitignore".into(), "ignored-dir\n".into()),
+            ("tracked-dir/tracked-file1".into(), "".into()),
+            ("tracked-dir/tracked-file2".into(), "".into()),
+        ],
+    );
+    fs.create_file(
+        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    fs.create_file(
+        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+
+    cx.executor().run_until_parked();
+    cx.read(|cx| {
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "tracked-dir/tracked-file2",
+            Some(StatusCode::Added),
+            false,
+        );
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "tracked-dir/ancestor-ignored-file2",
+            None,
+            false,
+        );
+        assert_entry_git_state(
+            tree.read(cx),
+            repository.read(cx),
+            "ignored-dir/ignored-file2",
+            None,
+            true,
+        );
+        assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
+    });
+}
+
 async fn search(
     project: &Entity<Project>,
     query: SearchQuery,
@@ -7303,3 +8122,143 @@ fn get_all_tasks(
     old.extend(new);
     old
 }
+
+#[track_caller]
+fn assert_entry_git_state(
+    tree: &Worktree,
+    repository: &Repository,
+    path: &str,
+    index_status: Option<StatusCode>,
+    is_ignored: bool,
+) {
+    assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
+    let entry = tree
+        .entry_for_path(path)
+        .unwrap_or_else(|| panic!("entry {path} not found"));
+    let status = repository
+        .status_for_path(&path.into())
+        .map(|entry| entry.status);
+    let expected = index_status.map(|index_status| {
+        TrackedStatus {
+            index_status,
+            worktree_status: StatusCode::Unmodified,
+        }
+        .into()
+    });
+    assert_eq!(
+        status, expected,
+        "expected {path} to have git status: {expected:?}"
+    );
+    assert_eq!(
+        entry.is_ignored, is_ignored,
+        "expected {path} to have is_ignored: {is_ignored}"
+    );
+}
+
+#[track_caller]
+fn git_init(path: &Path) -> git2::Repository {
+    let mut init_opts = RepositoryInitOptions::new();
+    init_opts.initial_head("main");
+    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
+}
+
+#[track_caller]
+fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
+    let path = path.as_ref();
+    let mut index = repo.index().expect("Failed to get index");
+    index.add_path(path).expect("Failed to add file");
+    index.write().expect("Failed to write index");
+}
+
+#[track_caller]
+fn git_remove_index(path: &Path, repo: &git2::Repository) {
+    let mut index = repo.index().expect("Failed to get index");
+    index.remove_path(path).expect("Failed to add file");
+    index.write().expect("Failed to write index");
+}
+
+#[track_caller]
+fn git_commit(msg: &'static str, repo: &git2::Repository) {
+    use git2::Signature;
+
+    let signature = Signature::now("test", "test@zed.dev").unwrap();
+    let oid = repo.index().unwrap().write_tree().unwrap();
+    let tree = repo.find_tree(oid).unwrap();
+    if let Ok(head) = repo.head() {
+        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
+
+        let parent_commit = parent_obj.as_commit().unwrap();
+
+        repo.commit(
+            Some("HEAD"),
+            &signature,
+            &signature,
+            msg,
+            &tree,
+            &[parent_commit],
+        )
+        .expect("Failed to commit with parent");
+    } else {
+        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
+            .expect("Failed to commit");
+    }
+}
+
+#[cfg(not(windows))]
+#[track_caller]
+fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
+    repo.cherrypick(commit, None).expect("Failed to cherrypick");
+}
+
+#[track_caller]
+fn git_stash(repo: &mut git2::Repository) {
+    use git2::Signature;
+
+    let signature = Signature::now("test", "test@zed.dev").unwrap();
+    repo.stash_save(&signature, "N/A", None)
+        .expect("Failed to stash");
+}
+
+#[track_caller]
+fn git_reset(offset: usize, repo: &git2::Repository) {
+    let head = repo.head().expect("Couldn't get repo head");
+    let object = head.peel(git2::ObjectType::Commit).unwrap();
+    let commit = object.as_commit().unwrap();
+    let new_head = commit
+        .parents()
+        .inspect(|parnet| {
+            parnet.message();
+        })
+        .nth(offset)
+        .expect("Not enough history");
+    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
+        .expect("Could not reset");
+}
+
+#[cfg(not(windows))]
+#[track_caller]
+fn git_branch(name: &str, repo: &git2::Repository) {
+    let head = repo
+        .head()
+        .expect("Couldn't get repo head")
+        .peel_to_commit()
+        .expect("HEAD is not a commit");
+    repo.branch(name, &head, false).expect("Failed to commit");
+}
+
+#[cfg(not(windows))]
+#[track_caller]
+fn git_checkout(name: &str, repo: &git2::Repository) {
+    repo.set_head(name).expect("Failed to set head");
+    repo.checkout_head(None).expect("Failed to check out head");
+}
+
+#[cfg(not(windows))]
+#[track_caller]
+fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
+    repo.statuses(None)
+        .unwrap()
+        .iter()
+        .map(|status| (status.path().unwrap().to_string(), status.status()))
+        .collect()
+}

crates/project/src/task_store.rs 🔗

@@ -298,7 +298,7 @@ fn local_task_context_for_location(
         let worktree_abs_path = worktree_abs_path.clone();
         let project_env = environment
             .update(cx, |environment, cx| {
-                environment.get_environment(worktree_id, worktree_abs_path.clone(), cx)
+                environment.get_environment(worktree_abs_path.clone(), cx)
             })
             .ok()?
             .await;

crates/project/src/toolchain_store.rs 🔗

@@ -331,11 +331,7 @@ impl LocalToolchainStore {
         cx.spawn(async move |cx| {
             let project_env = environment
                 .update(cx, |environment, cx| {
-                    environment.get_environment(
-                        Some(path.worktree_id),
-                        Some(Arc::from(abs_path.as_path())),
-                        cx,
-                    )
+                    environment.get_environment(Some(root.clone()), cx)
                 })
                 .ok()?
                 .await;

crates/project_panel/src/project_panel.rs 🔗

@@ -29,7 +29,8 @@ use language::DiagnosticSeverity;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::{
     Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
-    ProjectPath, Worktree, WorktreeId, git_store::git_traversal::ChildEntriesGitIter,
+    ProjectPath, Worktree, WorktreeId,
+    git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
     relativize_path,
 };
 use project_panel_settings::{
@@ -298,6 +299,7 @@ impl ProjectPanel {
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
         let project = workspace.project().clone();
+        let git_store = project.read(cx).git_store().clone();
         let project_panel = cx.new(|cx| {
             let focus_handle = cx.focus_handle();
             cx.on_focus(&focus_handle, window, Self::focus_in).detach();
@@ -306,6 +308,18 @@ impl ProjectPanel {
                 this.hide_scrollbar(window, cx);
             })
             .detach();
+
+            cx.subscribe(&git_store, |this, _, event, cx| match event {
+                GitStoreEvent::RepositoryUpdated(_, _, _)
+                | GitStoreEvent::RepositoryAdded(_)
+                | GitStoreEvent::RepositoryRemoved(_) => {
+                    this.update_visible_entries(None, cx);
+                    cx.notify();
+                }
+                _ => {}
+            })
+            .detach();
+
             cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::ActiveEntryChanged(Some(entry_id)) => {
                     if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
@@ -335,9 +349,7 @@ impl ProjectPanel {
                     this.update_visible_entries(None, cx);
                     cx.notify();
                 }
-                project::Event::GitStateUpdated
-                | project::Event::ActiveRepositoryChanged
-                | project::Event::WorktreeUpdatedEntries(_, _)
+                project::Event::WorktreeUpdatedEntries(_, _)
                 | project::Event::WorktreeAdded(_)
                 | project::Event::WorktreeOrderChanged => {
                     this.update_visible_entries(None, cx);

crates/proto/proto/zed.proto 🔗

@@ -1937,7 +1937,7 @@ message Entry {
 }
 
 message RepositoryEntry {
-    uint64 work_directory_id = 1;
+    uint64 repository_id = 1;
     reserved 2;
     repeated StatusEntry updated_statuses = 3;
     repeated string removed_statuses = 4;
@@ -1955,6 +1955,7 @@ message UpdateRepository {
     repeated string removed_statuses = 7;
     repeated string current_merge_conflicts = 8;
     uint64 scan_id = 9;
+    bool is_last_update = 10;
 }
 
 message RemoveRepository {
@@ -2247,7 +2248,7 @@ message OpenUncommittedDiffResponse {
 message SetIndexText {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string path = 4;
     optional string text = 5;
 }
@@ -3356,7 +3357,7 @@ message GetPanicFiles {
 message GitShow {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string commit = 4;
 }
 
@@ -3371,7 +3372,7 @@ message GitCommitDetails {
 message LoadCommitDiff {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string commit = 4;
 }
 
@@ -3388,7 +3389,7 @@ message CommitFile {
 message GitReset {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string commit = 4;
     ResetMode mode = 5;
     enum ResetMode {
@@ -3400,7 +3401,7 @@ message GitReset {
 message GitCheckoutFiles {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string commit = 4;
     repeated string paths = 5;
 }
@@ -3455,21 +3456,21 @@ message RegisterBufferWithLanguageServers{
 message Stage {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     repeated string paths = 4;
 }
 
 message Unstage {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     repeated string paths = 4;
 }
 
 message Commit {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     optional string name = 4;
     optional string email = 5;
     string message = 6;
@@ -3478,13 +3479,13 @@ message Commit {
 message OpenCommitMessageBuffer {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
 }
 
 message Push {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string remote_name = 4;
     string branch_name = 5;
     optional PushOptions options = 6;
@@ -3499,14 +3500,14 @@ message Push {
 message Fetch {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     uint64 askpass_id = 4;
 }
 
 message GetRemotes {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     optional string branch_name = 4;
 }
 
@@ -3521,7 +3522,7 @@ message GetRemotesResponse {
 message Pull {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string remote_name = 4;
     string branch_name = 5;
     uint64 askpass_id = 6;
@@ -3535,7 +3536,7 @@ message RemoteMessageResponse {
 message AskPassRequest {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     uint64 askpass_id = 4;
     string prompt = 5;
 }
@@ -3547,27 +3548,27 @@ message AskPassResponse {
 message GitGetBranches {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
 }
 
 message GitCreateBranch {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string branch_name = 4;
 }
 
 message GitChangeBranch {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     string branch_name = 4;
 }
 
 message CheckForPushedCommits {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
 }
 
 message CheckForPushedCommitsResponse {
@@ -3577,7 +3578,7 @@ message CheckForPushedCommitsResponse {
 message GitDiff {
     uint64 project_id = 1;
     reserved 2;
-    uint64 work_directory_id = 3;
+    uint64 repository_id = 3;
     DiffType diff_type = 4;
 
     enum DiffType {

crates/proto/src/proto.rs 🔗

@@ -834,7 +834,7 @@ pub fn split_worktree_update(mut message: UpdateWorktree) -> impl Iterator<Item
             let removed_statuses_limit = cmp::min(repo.removed_statuses.len(), limit);
 
             updated_repositories.push(RepositoryEntry {
-                work_directory_id: repo.work_directory_id,
+                repository_id: repo.repository_id,
                 branch_summary: repo.branch_summary.clone(),
                 updated_statuses: repo
                     .updated_statuses
@@ -885,26 +885,34 @@ pub fn split_repository_update(
 ) -> impl Iterator<Item = UpdateRepository> {
     let mut updated_statuses_iter = mem::take(&mut update.updated_statuses).into_iter().fuse();
     let mut removed_statuses_iter = mem::take(&mut update.removed_statuses).into_iter().fuse();
-    let mut is_first = true;
-    std::iter::from_fn(move || {
-        let updated_statuses = updated_statuses_iter
-            .by_ref()
-            .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
-            .collect::<Vec<_>>();
-        let removed_statuses = removed_statuses_iter
-            .by_ref()
-            .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
-            .collect::<Vec<_>>();
-        if updated_statuses.is_empty() && removed_statuses.is_empty() && !is_first {
-            return None;
+    std::iter::from_fn({
+        let update = update.clone();
+        move || {
+            let updated_statuses = updated_statuses_iter
+                .by_ref()
+                .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
+                .collect::<Vec<_>>();
+            let removed_statuses = removed_statuses_iter
+                .by_ref()
+                .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
+                .collect::<Vec<_>>();
+            if updated_statuses.is_empty() && removed_statuses.is_empty() {
+                return None;
+            }
+            Some(UpdateRepository {
+                updated_statuses,
+                removed_statuses,
+                is_last_update: false,
+                ..update.clone()
+            })
         }
-        is_first = false;
-        Some(UpdateRepository {
-            updated_statuses,
-            removed_statuses,
-            ..update.clone()
-        })
     })
+    .chain([UpdateRepository {
+        updated_statuses: Vec::new(),
+        removed_statuses: Vec::new(),
+        is_last_update: true,
+        ..update
+    }])
 }
 
 #[cfg(test)]

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -28,6 +28,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+#[cfg(not(windows))]
 use unindent::Unindent as _;
 use util::{path, separator};
 
@@ -1203,6 +1204,8 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
     });
 }
 
+// TODO: this test fails on Windows.
+#[cfg(not(windows))]
 #[gpui::test]
 async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let text_2 = "
@@ -1379,7 +1382,8 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
                     .next()
                     .unwrap()
                     .read(cx)
-                    .current_branch()
+                    .branch
+                    .as_ref()
                     .unwrap()
                     .clone()
             })
@@ -1418,7 +1422,8 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
                     .next()
                     .unwrap()
                     .read(cx)
-                    .current_branch()
+                    .branch
+                    .as_ref()
                     .unwrap()
                     .clone()
             })

crates/sum_tree/src/tree_map.rs 🔗

@@ -317,10 +317,18 @@ where
         ))
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
     pub fn insert(&mut self, key: K) {
         self.0.insert(key, ());
     }
 
+    pub fn remove(&mut self, key: &K) -> bool {
+        self.0.remove(key).is_some()
+    }
+
     pub fn extend(&mut self, iter: impl IntoIterator<Item = K>) {
         self.0.extend(iter.into_iter().map(|key| (key, ())));
     }

crates/title_bar/src/title_bar.rs 🔗

@@ -522,7 +522,7 @@ impl TitleBar {
     pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
         let repository = self.project.read(cx).active_repository(cx)?;
         let workspace = self.workspace.upgrade()?;
-        let branch_name = repository.read(cx).current_branch()?.name.clone();
+        let branch_name = repository.read(cx).branch.as_ref()?.name.clone();
         let branch_name = util::truncate_and_trailoff(&branch_name, MAX_BRANCH_NAME_LENGTH);
         Some(
             Button::new("project_branch_trigger", branch_name)

crates/vim/src/normal/search.rs 🔗

@@ -783,6 +783,7 @@ mod test {
     async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, false).await;
         cx.cx.set_state("ˇone one one one");
+        cx.run_until_parked();
         cx.simulate_keystrokes("cmd-f");
         cx.run_until_parked();
 

crates/worktree/Cargo.toml 🔗

@@ -30,7 +30,6 @@ fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true
-git_hosting_providers.workspace = true
 gpui.workspace = true
 ignore.workspace = true
 language.workspace = true

crates/worktree/src/worktree.rs 🔗

@@ -14,18 +14,13 @@ use futures::{
         mpsc::{self, UnboundedSender},
         oneshot,
     },
-    future::join_all,
     select_biased,
     task::Poll,
 };
 use fuzzy::CharBag;
 use git::{
-    COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, GitHostingProviderRegistry, INDEX_LOCK,
-    LFS_DIR,
-    repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
-    status::{
-        FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
-    },
+    COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR,
+    repository::RepoPath, status::GitSummary,
 };
 use gpui::{
     App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
@@ -62,7 +57,7 @@ use std::{
     pin::Pin,
     sync::{
         Arc,
-        atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicUsize, Ordering::SeqCst},
     },
     time::{Duration, Instant},
 };
@@ -159,7 +154,6 @@ pub struct Snapshot {
     entries_by_path: SumTree<Entry>,
     entries_by_id: SumTree<PathEntry>,
     always_included_entries: Vec<Arc<Path>>,
-    repositories: SumTree<RepositoryEntry>,
 
     /// A number that increases every time the worktree begins scanning
     /// a set of paths from the filesystem. This scanning could be caused
@@ -174,223 +168,6 @@ pub struct Snapshot {
     completed_scan_id: usize,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RepositoryEntry {
-    /// The git status entries for this repository.
-    /// Note that the paths on this repository are relative to the git work directory.
-    /// If the .git folder is external to Zed, these paths will be relative to that folder,
-    /// and this data structure might reference files external to this worktree.
-    ///
-    /// For example:
-    ///
-    ///     my_root_folder/          <-- repository root
-    ///       .git
-    ///       my_sub_folder_1/
-    ///         project_root/        <-- Project root, Zed opened here
-    ///           changed_file_1     <-- File with changes, in worktree
-    ///       my_sub_folder_2/
-    ///         changed_file_2       <-- File with changes, out of worktree
-    ///           ...
-    ///
-    /// With this setup, this field would contain 2 entries, like so:
-    ///     - my_sub_folder_1/project_root/changed_file_1
-    ///     - my_sub_folder_2/changed_file_2
-    pub statuses_by_path: SumTree<StatusEntry>,
-    pub work_directory_id: ProjectEntryId,
-    pub work_directory_abs_path: PathBuf,
-    pub worktree_scan_id: usize,
-    pub current_branch: Option<Branch>,
-    pub current_merge_conflicts: TreeSet<RepoPath>,
-}
-
-impl RepositoryEntry {
-    pub fn relativize_abs_path(&self, abs_path: &Path) -> Option<RepoPath> {
-        Some(
-            abs_path
-                .strip_prefix(&self.work_directory_abs_path)
-                .ok()?
-                .into(),
-        )
-    }
-
-    pub fn directory_contains_abs_path(&self, abs_path: impl AsRef<Path>) -> bool {
-        abs_path.as_ref().starts_with(&self.work_directory_abs_path)
-    }
-
-    pub fn branch(&self) -> Option<&Branch> {
-        self.current_branch.as_ref()
-    }
-
-    pub fn work_directory_id(&self) -> ProjectEntryId {
-        self.work_directory_id
-    }
-
-    pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
-        self.statuses_by_path.iter().cloned()
-    }
-
-    pub fn status_len(&self) -> usize {
-        self.statuses_by_path.summary().item_summary.count
-    }
-
-    pub fn status_summary(&self) -> GitSummary {
-        self.statuses_by_path.summary().item_summary
-    }
-
-    pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
-        self.statuses_by_path
-            .get(&PathKey(path.0.clone()), &())
-            .cloned()
-    }
-
-    pub fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
-        proto::UpdateRepository {
-            branch_summary: self.current_branch.as_ref().map(branch_to_proto),
-            updated_statuses: self
-                .statuses_by_path
-                .iter()
-                .map(|entry| entry.to_proto())
-                .collect(),
-            removed_statuses: Default::default(),
-            current_merge_conflicts: self
-                .current_merge_conflicts
-                .iter()
-                .map(|repo_path| repo_path.to_proto())
-                .collect(),
-            project_id,
-            // This is semantically wrong---we want to move to having separate IDs for repositories.
-            // But for the moment, RepositoryEntry isn't set up to provide that at this level, so we
-            // shim it using the work directory's project entry ID. The pair of this + project ID will
-            // be globally unique.
-            id: self.work_directory_id().to_proto(),
-            abs_path: self.work_directory_abs_path.as_path().to_proto(),
-            entry_ids: vec![self.work_directory_id().to_proto()],
-            // This is also semantically wrong, and should be replaced once we separate git repo updates
-            // from worktree scans.
-            scan_id: self.worktree_scan_id as u64,
-        }
-    }
-
-    pub fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {
-        let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
-        let mut removed_statuses: Vec<String> = Vec::new();
-
-        let mut new_statuses = self.statuses_by_path.iter().peekable();
-        let mut old_statuses = old.statuses_by_path.iter().peekable();
-
-        let mut current_new_entry = new_statuses.next();
-        let mut current_old_entry = old_statuses.next();
-        loop {
-            match (current_new_entry, current_old_entry) {
-                (Some(new_entry), Some(old_entry)) => {
-                    match new_entry.repo_path.cmp(&old_entry.repo_path) {
-                        Ordering::Less => {
-                            updated_statuses.push(new_entry.to_proto());
-                            current_new_entry = new_statuses.next();
-                        }
-                        Ordering::Equal => {
-                            if new_entry.status != old_entry.status {
-                                updated_statuses.push(new_entry.to_proto());
-                            }
-                            current_old_entry = old_statuses.next();
-                            current_new_entry = new_statuses.next();
-                        }
-                        Ordering::Greater => {
-                            removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
-                            current_old_entry = old_statuses.next();
-                        }
-                    }
-                }
-                (None, Some(old_entry)) => {
-                    removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
-                    current_old_entry = old_statuses.next();
-                }
-                (Some(new_entry), None) => {
-                    updated_statuses.push(new_entry.to_proto());
-                    current_new_entry = new_statuses.next();
-                }
-                (None, None) => break,
-            }
-        }
-
-        proto::UpdateRepository {
-            branch_summary: self.current_branch.as_ref().map(branch_to_proto),
-            updated_statuses,
-            removed_statuses,
-            current_merge_conflicts: self
-                .current_merge_conflicts
-                .iter()
-                .map(|path| path.as_ref().to_proto())
-                .collect(),
-            project_id,
-            id: self.work_directory_id.to_proto(),
-            abs_path: self.work_directory_abs_path.as_path().to_proto(),
-            entry_ids: vec![self.work_directory_id.to_proto()],
-            scan_id: self.worktree_scan_id as u64,
-        }
-    }
-}
-
-pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
-    proto::Branch {
-        is_head: branch.is_head,
-        name: branch.name.to_string(),
-        unix_timestamp: branch
-            .most_recent_commit
-            .as_ref()
-            .map(|commit| commit.commit_timestamp as u64),
-        upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {
-            ref_name: upstream.ref_name.to_string(),
-            tracking: upstream
-                .tracking
-                .status()
-                .map(|upstream| proto::UpstreamTracking {
-                    ahead: upstream.ahead as u64,
-                    behind: upstream.behind as u64,
-                }),
-        }),
-        most_recent_commit: branch
-            .most_recent_commit
-            .as_ref()
-            .map(|commit| proto::CommitSummary {
-                sha: commit.sha.to_string(),
-                subject: commit.subject.to_string(),
-                commit_timestamp: commit.commit_timestamp,
-            }),
-    }
-}
-
-pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
-    git::repository::Branch {
-        is_head: proto.is_head,
-        name: proto.name.clone().into(),
-        upstream: proto
-            .upstream
-            .as_ref()
-            .map(|upstream| git::repository::Upstream {
-                ref_name: upstream.ref_name.to_string().into(),
-                tracking: upstream
-                    .tracking
-                    .as_ref()
-                    .map(|tracking| {
-                        git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
-                            ahead: tracking.ahead as u32,
-                            behind: tracking.behind as u32,
-                        })
-                    })
-                    .unwrap_or(git::repository::UpstreamTracking::Gone),
-            }),
-        most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
-            git::repository::CommitSummary {
-                sha: commit.sha.to_string().into(),
-                subject: commit.subject.to_string().into(),
-                commit_timestamp: commit.commit_timestamp,
-                has_parent: true,
-            }
-        }),
-    }
-}
-
 /// This path corresponds to the 'content path' of a repository in relation
 /// to Zed's project root.
 /// In the majority of the cases, this is the folder that contains the .git folder.
@@ -598,24 +375,20 @@ struct BackgroundScannerState {
     removed_entries: HashMap<u64, Entry>,
     changed_paths: Vec<Arc<Path>>,
     prev_snapshot: Snapshot,
-    git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>,
-    repository_scans: HashMap<PathKey, Task<()>>,
 }
 
 #[derive(Debug, Clone)]
-pub struct LocalRepositoryEntry {
-    pub(crate) work_directory_id: ProjectEntryId,
-    pub(crate) work_directory: WorkDirectory,
-    pub(crate) git_dir_scan_id: usize,
-    pub(crate) status_scan_id: usize,
-    pub(crate) repo_ptr: Arc<dyn GitRepository>,
+struct LocalRepositoryEntry {
+    work_directory_id: ProjectEntryId,
+    work_directory: WorkDirectory,
+    work_directory_abs_path: Arc<Path>,
+    git_dir_scan_id: usize,
+    original_dot_git_abs_path: Arc<Path>,
     /// Absolute path to the actual .git folder.
     /// Note: if .git is a file, this points to the folder indicated by the .git file
-    pub(crate) dot_git_dir_abs_path: Arc<Path>,
+    dot_git_dir_abs_path: Arc<Path>,
     /// Absolute path to the .git file, if we're in a git worktree.
-    pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>,
-    pub current_merge_head_shas: Vec<String>,
-    pub merge_message: Option<String>,
+    dot_git_worktree_abs_path: Option<Arc<Path>>,
 }
 
 impl sum_tree::Item for LocalRepositoryEntry {
@@ -637,11 +410,11 @@ impl KeyedItem for LocalRepositoryEntry {
     }
 }
 
-impl LocalRepositoryEntry {
-    pub fn repo(&self) -> &Arc<dyn GitRepository> {
-        &self.repo_ptr
-    }
-}
+//impl LocalRepositoryEntry {
+//    pub fn repo(&self) -> &Arc<dyn GitRepository> {
+//        &self.repo_ptr
+//    }
+//}
 
 impl Deref for LocalRepositoryEntry {
     type Target = WorkDirectory;
@@ -1030,54 +803,6 @@ impl Worktree {
         }
     }
 
-    pub fn load_staged_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
-        match self {
-            Worktree::Local(this) => {
-                let path = Arc::from(path);
-                let snapshot = this.snapshot();
-                cx.spawn(async move |_cx| {
-                    if let Some(repo) = snapshot.local_repo_containing_path(&path) {
-                        if let Some(repo_path) = repo.relativize(&path).log_err() {
-                            if let Some(git_repo) =
-                                snapshot.git_repositories.get(&repo.work_directory_id)
-                            {
-                                return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
-                            }
-                        }
-                    }
-                    Err(anyhow!("No repository found for {path:?}"))
-                })
-            }
-            Worktree::Remote(_) => {
-                Task::ready(Err(anyhow!("remote worktrees can't yet load staged files")))
-            }
-        }
-    }
-
-    pub fn load_committed_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
-        match self {
-            Worktree::Local(this) => {
-                let path = Arc::from(path);
-                let snapshot = this.snapshot();
-                cx.spawn(async move |_cx| {
-                    if let Some(repo) = snapshot.local_repo_containing_path(&path) {
-                        if let Some(repo_path) = repo.relativize(&path).log_err() {
-                            if let Some(git_repo) =
-                                snapshot.git_repositories.get(&repo.work_directory_id)
-                            {
-                                return Ok(git_repo.repo_ptr.load_committed_text(repo_path).await);
-                            }
-                        }
-                    }
-                    Err(anyhow!("No repository found for {path:?}"))
-                })
-            }
-            Worktree::Remote(_) => Task::ready(Err(anyhow!(
-                "remote worktrees can't yet load committed files"
-            ))),
-        }
-    }
-
     pub fn load_binary_file(
         &self,
         path: &Path,
@@ -1485,7 +1210,6 @@ impl LocalWorktree {
         let share_private_files = self.share_private_files;
         let next_entry_id = self.next_entry_id.clone();
         let fs = self.fs.clone();
-        let git_hosting_provider_registry = GitHostingProviderRegistry::try_global(cx);
         let settings = self.settings.clone();
         let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
         let background_scanner = cx.background_spawn({
@@ -1502,12 +1226,11 @@ impl LocalWorktree {
                     fs,
                     fs_case_sensitive,
                     status_updates_tx: scan_states_tx,
-                    scans_running: Arc::new(AtomicI32::new(0)),
                     executor: background,
                     scan_requests_rx,
                     path_prefixes_to_scan_rx,
                     next_entry_id,
-                    state: Arc::new(Mutex::new(BackgroundScannerState {
+                    state: Mutex::new(BackgroundScannerState {
                         prev_snapshot: snapshot.snapshot.clone(),
                         snapshot,
                         scanned_dirs: Default::default(),
@@ -1515,9 +1238,7 @@ impl LocalWorktree {
                         paths_to_scan: Default::default(),
                         removed_entries: Default::default(),
                         changed_paths: Default::default(),
-                        repository_scans: HashMap::default(),
-                        git_hosting_provider_registry,
-                    })),
+                    }),
                     phase: BackgroundScannerPhase::InitialScan,
                     share_private_files,
                     settings,
@@ -1561,11 +1282,11 @@ impl LocalWorktree {
 
     fn set_snapshot(
         &mut self,
-        new_snapshot: LocalSnapshot,
+        mut new_snapshot: LocalSnapshot,
         entry_changes: UpdatedEntriesSet,
         cx: &mut Context<Worktree>,
     ) {
-        let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot);
+        let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot);
         self.snapshot = new_snapshot;
 
         if let Some(share) = self.update_observer.as_mut() {
@@ -1586,81 +1307,78 @@ impl LocalWorktree {
     fn changed_repos(
         &self,
         old_snapshot: &LocalSnapshot,
-        new_snapshot: &LocalSnapshot,
+        new_snapshot: &mut LocalSnapshot,
     ) -> UpdatedGitRepositoriesSet {
         let mut changes = Vec::new();
         let mut old_repos = old_snapshot.git_repositories.iter().peekable();
-        let mut new_repos = new_snapshot.git_repositories.iter().peekable();
+        let new_repos = new_snapshot.git_repositories.clone();
+        let mut new_repos = new_repos.iter().peekable();
 
         loop {
             match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
                 (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
                     match Ord::cmp(&new_entry_id, &old_entry_id) {
                         Ordering::Less => {
-                            if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
-                                changes.push((
-                                    entry.clone(),
-                                    GitRepositoryChange {
-                                        old_repository: None,
-                                    },
-                                ));
-                            }
+                            changes.push(UpdatedGitRepository {
+                                work_directory_id: new_entry_id,
+                                old_work_directory_abs_path: None,
+                                new_work_directory_abs_path: Some(
+                                    new_repo.work_directory_abs_path.clone(),
+                                ),
+                                dot_git_abs_path: Some(new_repo.original_dot_git_abs_path.clone()),
+                            });
                             new_repos.next();
                         }
                         Ordering::Equal => {
                             if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id
-                                || new_repo.status_scan_id != old_repo.status_scan_id
+                                || new_repo.work_directory_abs_path
+                                    != old_repo.work_directory_abs_path
                             {
-                                if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
-                                    let old_repo =
-                                        old_snapshot.repository_for_id(old_entry_id).cloned();
-                                    changes.push((
-                                        entry.clone(),
-                                        GitRepositoryChange {
-                                            old_repository: old_repo,
-                                        },
-                                    ));
-                                }
+                                changes.push(UpdatedGitRepository {
+                                    work_directory_id: new_entry_id,
+                                    old_work_directory_abs_path: Some(
+                                        old_repo.work_directory_abs_path.clone(),
+                                    ),
+                                    new_work_directory_abs_path: Some(
+                                        new_repo.work_directory_abs_path.clone(),
+                                    ),
+                                    dot_git_abs_path: Some(
+                                        new_repo.original_dot_git_abs_path.clone(),
+                                    ),
+                                });
                             }
                             new_repos.next();
                             old_repos.next();
                         }
                         Ordering::Greater => {
-                            if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) {
-                                let old_repo =
-                                    old_snapshot.repository_for_id(old_entry_id).cloned();
-                                changes.push((
-                                    entry.clone(),
-                                    GitRepositoryChange {
-                                        old_repository: old_repo,
-                                    },
-                                ));
-                            }
+                            changes.push(UpdatedGitRepository {
+                                work_directory_id: old_entry_id,
+                                old_work_directory_abs_path: Some(
+                                    old_repo.work_directory_abs_path.clone(),
+                                ),
+                                new_work_directory_abs_path: None,
+                                dot_git_abs_path: None,
+                            });
                             old_repos.next();
                         }
                     }
                 }
-                (Some((entry_id, _)), None) => {
-                    if let Some(entry) = new_snapshot.entry_for_id(entry_id) {
-                        changes.push((
-                            entry.clone(),
-                            GitRepositoryChange {
-                                old_repository: None,
-                            },
-                        ));
-                    }
+                (Some((entry_id, repo)), None) => {
+                    changes.push(UpdatedGitRepository {
+                        work_directory_id: entry_id,
+                        old_work_directory_abs_path: None,
+                        new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
+                        dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
+                    });
                     new_repos.next();
                 }
-                (None, Some((entry_id, _))) => {
-                    if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
-                        let old_repo = old_snapshot.repository_for_id(entry_id).cloned();
-                        changes.push((
-                            entry.clone(),
-                            GitRepositoryChange {
-                                old_repository: old_repo,
-                            },
-                        ));
-                    }
+                (None, Some((entry_id, repo))) => {
+                    changes.push(UpdatedGitRepository {
+                        work_directory_id: entry_id,
+                        old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
+                        new_work_directory_abs_path: None,
+                        dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
+                    });
                     old_repos.next();
                 }
                 (None, None) => break,
@@ -1696,10 +1414,6 @@ impl LocalWorktree {
         self.settings.clone()
     }
 
-    pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
-        self.git_repositories.get(&repo.work_directory_id)
-    }
-
     fn load_binary_file(
         &self,
         path: &Path,
@@ -2228,6 +1942,11 @@ impl LocalWorktree {
         rx
     }
 
+    #[cfg(feature = "test-support")]
+    pub fn manually_refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+        self.refresh_entries_for_paths(paths)
+    }
+
     pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
         let (tx, rx) = barrier::channel();
         self.path_prefixes_to_scan_tx
@@ -2527,7 +2246,6 @@ impl Snapshot {
             always_included_entries: Default::default(),
             entries_by_path: Default::default(),
             entries_by_id: Default::default(),
-            repositories: Default::default(),
             scan_id: 1,
             completed_scan_id: 0,
         }
@@ -2646,26 +2364,6 @@ impl Snapshot {
         Some(removed_entry.path)
     }
 
-    //#[cfg(any(test, feature = "test-support"))]
-    //pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<FileStatus> {
-    //    let path = path.as_ref();
-    //    self.repository_for_path(path).and_then(|repo| {
-    //        let repo_path = repo.relativize(path).unwrap();
-    //        repo.statuses_by_path
-    //            .get(&PathKey(repo_path.0), &())
-    //            .map(|entry| entry.status)
-    //    })
-    //}
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn status_for_file_abs_path(&self, abs_path: impl AsRef<Path>) -> Option<FileStatus> {
-        let abs_path = abs_path.as_ref();
-        let repo = self.repository_containing_abs_path(abs_path)?;
-        let repo_path = repo.relativize_abs_path(abs_path)?;
-        let status = repo.statuses_by_path.get(&PathKey(repo_path.0), &())?;
-        Some(status.status)
-    }
-
     fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
         self.abs_path = abs_path;
         if root_name != self.root_name {
@@ -2674,7 +2372,7 @@ impl Snapshot {
         }
     }
 
-    pub(crate) fn apply_remote_update(
+    fn apply_remote_update(
         &mut self,
         update: proto::UpdateWorktree,
         always_included_paths: &PathMatcher,
@@ -2805,24 +2503,6 @@ impl Snapshot {
         self.traverse_from_offset(true, true, include_ignored, start)
     }
 
-    pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
-        &self.repositories
-    }
-
-    /// Get the repository whose work directory contains the given path.
-    fn repository_containing_abs_path(&self, abs_path: &Path) -> Option<&RepositoryEntry> {
-        self.repositories
-            .iter()
-            .filter(|repo| repo.directory_contains_abs_path(abs_path))
-            .last()
-    }
-
-    fn repository_for_id(&self, id: ProjectEntryId) -> Option<&RepositoryEntry> {
-        self.repositories
-            .iter()
-            .find(|repo| repo.work_directory_id == id)
-    }
-
     pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
         let empty_path = Path::new("");
         self.entries_by_path
@@ -2905,20 +2585,13 @@ impl Snapshot {
 }
 
 impl LocalSnapshot {
-    pub fn local_repo_for_work_directory_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
+    fn local_repo_for_work_directory_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
         self.git_repositories
             .iter()
             .map(|(_, entry)| entry)
             .find(|entry| entry.work_directory.path_key() == PathKey(path.into()))
     }
 
-    pub fn local_repo_containing_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
-        self.git_repositories
-            .values()
-            .filter(|local_repo| path.starts_with(&local_repo.path_key().0))
-            .max_by_key(|local_repo| local_repo.path_key())
-    }
-
     fn build_update(
         &self,
         project_id: u64,
@@ -3046,7 +2719,7 @@ impl LocalSnapshot {
     }
 
     #[cfg(test)]
-    pub(crate) fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
+    fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
         self.entries_by_path
             .cursor::<()>(&())
             .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
@@ -3125,26 +2798,6 @@ impl LocalSnapshot {
         }
     }
 
-    #[cfg(test)]
-    fn check_git_invariants(&self) {
-        let dotgit_paths = self
-            .git_repositories
-            .iter()
-            .map(|repo| repo.1.dot_git_dir_abs_path.clone())
-            .collect::<HashSet<_>>();
-        let work_dir_paths = self
-            .repositories
-            .iter()
-            .map(|repo| repo.work_directory_abs_path.clone())
-            .collect::<HashSet<_>>();
-        assert_eq!(dotgit_paths.len(), work_dir_paths.len());
-        assert_eq!(self.repositories.iter().count(), work_dir_paths.len());
-        assert_eq!(self.git_repositories.iter().count(), work_dir_paths.len());
-        for entry in self.repositories.iter() {
-            self.git_repositories.get(&entry.work_directory_id).unwrap();
-        }
-    }
-
     #[cfg(test)]
     pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
         let mut paths = Vec::new();
@@ -3288,7 +2941,7 @@ impl BackgroundScannerState {
     }
 
     fn remove_path(&mut self, path: &Path) {
-        log::info!("background scanner removing path {path:?}");
+        log::debug!("background scanner removing path {path:?}");
         let mut new_entries;
         let removed_entries;
         {
@@ -3343,11 +2996,6 @@ impl BackgroundScannerState {
         self.snapshot
             .git_repositories
             .retain(|id, _| removed_ids.binary_search(id).is_err());
-        self.snapshot.repositories.retain(&(), |repository| {
-            removed_ids
-                .binary_search(&repository.work_directory_id)
-                .is_err()
-        });
 
         #[cfg(test)]
         self.snapshot.check_invariants(false);
@@ -3358,7 +3006,7 @@ impl BackgroundScannerState {
         dot_git_path: Arc<Path>,
         fs: &dyn Fs,
         watcher: &dyn Watcher,
-    ) -> Option<LocalRepositoryEntry> {
+    ) {
         let work_dir_path: Arc<Path> = match dot_git_path.parent() {
             Some(parent_dir) => {
                 // Guard against repositories inside the repository metadata
@@ -3366,7 +3014,7 @@ impl BackgroundScannerState {
                     log::info!(
                         "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
                     );
-                    return None;
+                    return;
                 };
                 log::info!(
                     "building git repository, `.git` path in the worktree: {dot_git_path:?}"
@@ -3380,7 +3028,7 @@ impl BackgroundScannerState {
                 log::info!(
                     "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
                 );
-                return None;
+                return;
             }
         };
 
@@ -3391,7 +3039,7 @@ impl BackgroundScannerState {
             dot_git_path,
             fs,
             watcher,
-        )
+        );
     }
 
     fn insert_git_repository_for_path(
@@ -3401,7 +3049,6 @@ impl BackgroundScannerState {
         fs: &dyn Fs,
         watcher: &dyn Watcher,
     ) -> Option<LocalRepositoryEntry> {
-        // TODO canonicalize here
         log::info!("insert git repository for {dot_git_path:?}");
         let work_dir_entry = self.snapshot.entry_for_path(work_directory.path_key().0)?;
         let work_directory_abs_path = self
@@ -3421,6 +3068,7 @@ impl BackgroundScannerState {
 
         let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
 
+        // TODO add these watchers without building a whole repository by parsing .git-with-indirection
         let t0 = Instant::now();
         let repository = fs.open_repo(&dot_git_abs_path)?;
         log::info!("opened git repo for {dot_git_abs_path:?}");
@@ -3443,41 +3091,21 @@ impl BackgroundScannerState {
             // * `actual_dot_git_dir_abs_path` is the path to the actual .git directory. In git
             // documentation this is called the "commondir".
             watcher.add(&dot_git_abs_path).log_err()?;
-            Some(Arc::from(dot_git_abs_path))
+            Some(Arc::from(dot_git_abs_path.as_path()))
         };
 
         log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
 
-        if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
-            git_hosting_providers::register_additional_providers(
-                git_hosting_provider_registry,
-                repository.clone(),
-            );
-        }
-
         let work_directory_id = work_dir_entry.id;
-        self.snapshot.repositories.insert_or_replace(
-            RepositoryEntry {
-                work_directory_id,
-                work_directory_abs_path,
-                current_branch: None,
-                statuses_by_path: Default::default(),
-                current_merge_conflicts: Default::default(),
-                worktree_scan_id: 0,
-            },
-            &(),
-        );
 
         let local_repository = LocalRepositoryEntry {
             work_directory_id,
             work_directory,
             git_dir_scan_id: 0,
-            status_scan_id: 0,
-            repo_ptr: repository.clone(),
+            original_dot_git_abs_path: dot_git_abs_path.as_path().into(),
             dot_git_dir_abs_path: actual_dot_git_dir_abs_path.into(),
+            work_directory_abs_path: work_directory_abs_path.as_path().into(),
             dot_git_worktree_abs_path,
-            current_merge_head_shas: Default::default(),
-            merge_message: None,
         };
 
         self.snapshot
@@ -3808,53 +3436,22 @@ pub enum PathChange {
     Loaded,
 }
 
-#[derive(Debug)]
-pub struct GitRepositoryChange {
-    /// The previous state of the repository, if it already existed.
-    pub old_repository: Option<RepositoryEntry>,
-}
-
-pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
-pub type UpdatedGitRepositoriesSet = Arc<[(Entry, GitRepositoryChange)]>;
-
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub struct StatusEntry {
-    pub repo_path: RepoPath,
-    pub status: FileStatus,
-}
-
-impl StatusEntry {
-    fn to_proto(&self) -> proto::StatusEntry {
-        let simple_status = match self.status {
-            FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
-            FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
-            FileStatus::Tracked(TrackedStatus {
-                index_status,
-                worktree_status,
-            }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
-                worktree_status
-            } else {
-                index_status
-            }),
-        };
-
-        proto::StatusEntry {
-            repo_path: self.repo_path.as_ref().to_proto(),
-            simple_status,
-            status: Some(status_to_proto(self.status)),
-        }
-    }
+pub struct UpdatedGitRepository {
+    /// ID of the repository's working directory.
+    ///
+    /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique.
+    /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed.
+    pub work_directory_id: ProjectEntryId,
+    pub old_work_directory_abs_path: Option<Arc<Path>>,
+    pub new_work_directory_abs_path: Option<Arc<Path>>,
+    /// For a normal git repository checkout, the absolute path to the .git directory.
+    /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
+    pub dot_git_abs_path: Option<Arc<Path>>,
 }
 
-impl TryFrom<proto::StatusEntry> for StatusEntry {
-    type Error = anyhow::Error;
-
-    fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
-        let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
-        let status = status_from_proto(value.simple_status, value.status)?;
-        Ok(Self { repo_path, status })
-    }
-}
+pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
+pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>;
 
 #[derive(Clone, Debug)]
 pub struct PathProgress<'a> {
@@ -3863,8 +3460,8 @@ pub struct PathProgress<'a> {
 
 #[derive(Clone, Debug)]
 pub struct PathSummary<S> {
-    max_path: Arc<Path>,
-    item_summary: S,
+    pub max_path: Arc<Path>,
+    pub item_summary: S,
 }
 
 impl<S: Summary> Summary for PathSummary<S> {
@@ -3899,75 +3496,6 @@ impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathProgress<'a
     }
 }
 
-#[derive(Clone, Debug)]
-pub struct AbsPathSummary {
-    max_path: Arc<Path>,
-}
-
-impl Summary for AbsPathSummary {
-    type Context = ();
-
-    fn zero(_: &Self::Context) -> Self {
-        Self {
-            max_path: Path::new("").into(),
-        }
-    }
-
-    fn add_summary(&mut self, rhs: &Self, _: &Self::Context) {
-        self.max_path = rhs.max_path.clone();
-    }
-}
-
-impl sum_tree::Item for RepositoryEntry {
-    type Summary = AbsPathSummary;
-
-    fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
-        AbsPathSummary {
-            max_path: self.work_directory_abs_path.as_path().into(),
-        }
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct AbsPathKey(pub Arc<Path>);
-
-impl<'a> sum_tree::Dimension<'a, AbsPathSummary> for AbsPathKey {
-    fn zero(_: &()) -> Self {
-        Self(Path::new("").into())
-    }
-
-    fn add_summary(&mut self, summary: &'a AbsPathSummary, _: &()) {
-        self.0 = summary.max_path.clone();
-    }
-}
-
-impl sum_tree::KeyedItem for RepositoryEntry {
-    type Key = AbsPathKey;
-
-    fn key(&self) -> Self::Key {
-        AbsPathKey(self.work_directory_abs_path.as_path().into())
-    }
-}
-
-impl sum_tree::Item for StatusEntry {
-    type Summary = PathSummary<GitSummary>;
-
-    fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
-        PathSummary {
-            max_path: self.repo_path.0.clone(),
-            item_summary: self.status.summary(),
-        }
-    }
-}
-
-impl sum_tree::KeyedItem for StatusEntry {
-    type Key = PathKey;
-
-    fn key(&self) -> Self::Key {
-        PathKey(self.repo_path.0.clone())
-    }
-}
-
 impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
     fn zero(_cx: &()) -> Self {
         Default::default()
@@ -3978,6 +3506,14 @@ impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
     }
 }
 
+impl<'a> sum_tree::SeekTarget<'a, PathSummary<GitSummary>, (TraversalProgress<'a>, GitSummary)>
+    for PathTarget<'_>
+{
+    fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering {
+        self.cmp_path(&cursor_location.0.max_path)
+    }
+}
+
 impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathKey {
     fn zero(_: &S::Context) -> Self {
         Default::default()
@@ -4204,11 +3740,10 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
 }
 
 struct BackgroundScanner {
-    state: Arc<Mutex<BackgroundScannerState>>,
+    state: Mutex<BackgroundScannerState>,
     fs: Arc<dyn Fs>,
     fs_case_sensitive: bool,
     status_updates_tx: UnboundedSender<ScanState>,
-    scans_running: Arc<AtomicI32>,
     executor: BackgroundExecutor,
     scan_requests_rx: channel::Receiver<ScanRequest>,
     path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
@@ -4322,8 +3857,7 @@ impl BackgroundScanner {
             state.snapshot.completed_scan_id = state.snapshot.scan_id;
         }
 
-        let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
-        self.send_status_update(scanning, SmallVec::new());
+        self.send_status_update(false, SmallVec::new());
 
         // Process any any FS events that occurred while performing the initial scan.
         // For these events, update events cannot be as precise, because we didn't

crates/worktree/src/worktree_tests.rs 🔗

@@ -1,15 +1,10 @@
 use crate::{
-    Entry, EntryKind, Event, PathChange, StatusEntry, WorkDirectory, Worktree, WorktreeModelHandle,
+    Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
     worktree_settings::WorktreeSettings,
 };
 use anyhow::Result;
 use fs::{FakeFs, Fs, RealFs, RemoveOptions};
-use git::{
-    GITIGNORE,
-    repository::RepoPath,
-    status::{FileStatus, StatusCode, TrackedStatus},
-};
-use git2::RepositoryInitOptions;
+use git::GITIGNORE;
 use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
 use parking_lot::Mutex;
 use postage::stream::Stream;
@@ -685,183 +680,6 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
     assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
 }
 
-#[gpui::test(iterations = 10)]
-async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.update(|cx| {
-        cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(Vec::new());
-            });
-        });
-    });
-    let fs = FakeFs::new(cx.background_executor.clone());
-    fs.insert_tree(
-        path!("/root"),
-        json!({
-            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
-            "tree": {
-                ".git": {},
-                ".gitignore": "ignored-dir\n",
-                "tracked-dir": {
-                    "tracked-file1": "",
-                    "ancestor-ignored-file1": "",
-                },
-                "ignored-dir": {
-                    "ignored-file1": ""
-                }
-            }
-        }),
-    )
-    .await;
-    fs.set_head_and_index_for_repo(
-        path!("/root/tree/.git").as_ref(),
-        &[
-            (".gitignore".into(), "ignored-dir\n".into()),
-            ("tracked-dir/tracked-file1".into(), "".into()),
-        ],
-    );
-
-    let tree = Worktree::local(
-        path!("/root/tree").as_ref(),
-        true,
-        fs.clone(),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    tree.read_with(cx, |tree, _| {
-        tree.as_local()
-            .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
-    })
-    .recv()
-    .await;
-
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
-        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
-        assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
-    });
-
-    fs.create_file(
-        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
-        Default::default(),
-    )
-    .await
-    .unwrap();
-    fs.set_index_for_repo(
-        path!("/root/tree/.git").as_ref(),
-        &[
-            (".gitignore".into(), "ignored-dir\n".into()),
-            ("tracked-dir/tracked-file1".into(), "".into()),
-            ("tracked-dir/tracked-file2".into(), "".into()),
-        ],
-    );
-    fs.create_file(
-        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
-        Default::default(),
-    )
-    .await
-    .unwrap();
-    fs.create_file(
-        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
-        Default::default(),
-    )
-    .await
-    .unwrap();
-
-    cx.executor().run_until_parked();
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        assert_entry_git_state(
-            tree,
-            "tracked-dir/tracked-file2",
-            Some(StatusCode::Added),
-            false,
-        );
-        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
-        assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
-        assert!(tree.entry_for_path(".git").unwrap().is_ignored);
-    });
-}
-
-#[gpui::test]
-async fn test_update_gitignore(cx: &mut TestAppContext) {
-    init_test(cx);
-    let fs = FakeFs::new(cx.background_executor.clone());
-    fs.insert_tree(
-        path!("/root"),
-        json!({
-            ".git": {},
-            ".gitignore": "*.txt\n",
-            "a.xml": "<a></a>",
-            "b.txt": "Some text"
-        }),
-    )
-    .await;
-
-    fs.set_head_and_index_for_repo(
-        path!("/root/.git").as_ref(),
-        &[
-            (".gitignore".into(), "*.txt\n".into()),
-            ("a.xml".into(), "<a></a>".into()),
-        ],
-    );
-
-    let tree = Worktree::local(
-        path!("/root").as_ref(),
-        true,
-        fs.clone(),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    tree.read_with(cx, |tree, _| {
-        tree.as_local()
-            .unwrap()
-            .refresh_entries_for_paths(vec![Path::new("").into()])
-    })
-    .recv()
-    .await;
-
-    // One file is unmodified, the other is ignored.
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        assert_entry_git_state(tree, "a.xml", None, false);
-        assert_entry_git_state(tree, "b.txt", None, true);
-    });
-
-    // Change the gitignore, and stage the newly non-ignored file.
-    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
-        .await
-        .unwrap();
-    fs.set_index_for_repo(
-        Path::new(path!("/root/.git")),
-        &[
-            (".gitignore".into(), "*.txt\n".into()),
-            ("a.xml".into(), "<a></a>".into()),
-            ("b.txt".into(), "Some text".into()),
-        ],
-    );
-
-    cx.executor().run_until_parked();
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        assert_entry_git_state(tree, "a.xml", None, true);
-        assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
-    });
-}
-
 #[gpui::test]
 async fn test_write_file(cx: &mut TestAppContext) {
     init_test(cx);
@@ -2106,655 +1924,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
         .collect()
 }
 
-// NOTE:
-// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
-// a directory which some program has already open.
-// This is a limitation of the Windows.
-// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
-#[gpui::test]
-#[cfg_attr(target_os = "windows", ignore)]
-async fn test_rename_work_directory(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-    let root = TempTree::new(json!({
-        "projects": {
-            "project1": {
-                "a": "",
-                "b": "",
-            }
-        },
-
-    }));
-    let root_path = root.path();
-
-    let tree = Worktree::local(
-        root_path,
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    let repo = git_init(&root_path.join("projects/project1"));
-    git_add("a", &repo);
-    git_commit("init", &repo);
-    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
-
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    tree.flush_fs_events(cx).await;
-
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        let repo = tree.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo.work_directory_abs_path,
-            root_path.join("projects/project1")
-        );
-        assert_eq!(
-            repo.status_for_path(&"a".into()).map(|entry| entry.status),
-            Some(StatusCode::Modified.worktree()),
-        );
-        assert_eq!(
-            repo.status_for_path(&"b".into()).map(|entry| entry.status),
-            Some(FileStatus::Untracked),
-        );
-    });
-
-    std::fs::rename(
-        root_path.join("projects/project1"),
-        root_path.join("projects/project2"),
-    )
-    .unwrap();
-    tree.flush_fs_events(cx).await;
-
-    cx.read(|cx| {
-        let tree = tree.read(cx);
-        let repo = tree.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo.work_directory_abs_path,
-            root_path.join("projects/project2")
-        );
-        assert_eq!(
-            repo.status_for_path(&"a".into()).unwrap().status,
-            StatusCode::Modified.worktree(),
-        );
-        assert_eq!(
-            repo.status_for_path(&"b".into()).unwrap().status,
-            FileStatus::Untracked,
-        );
-    });
-}
-
-// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
-// you can't rename a directory which some program has already open. This is a
-// limitation of the Windows. See:
-// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
-#[gpui::test]
-#[cfg_attr(target_os = "windows", ignore)]
-async fn test_file_status(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-    const IGNORE_RULE: &str = "**/target";
-
-    let root = TempTree::new(json!({
-        "project": {
-            "a.txt": "a",
-            "b.txt": "bb",
-            "c": {
-                "d": {
-                    "e.txt": "eee"
-                }
-            },
-            "f.txt": "ffff",
-            "target": {
-                "build_file": "???"
-            },
-            ".gitignore": IGNORE_RULE
-        },
-
-    }));
-
-    const A_TXT: &str = "a.txt";
-    const B_TXT: &str = "b.txt";
-    const E_TXT: &str = "c/d/e.txt";
-    const F_TXT: &str = "f.txt";
-    const DOTGITIGNORE: &str = ".gitignore";
-    const BUILD_FILE: &str = "target/build_file";
-
-    // Set up git repository before creating the worktree.
-    let work_dir = root.path().join("project");
-    let mut repo = git_init(work_dir.as_path());
-    repo.add_ignore_rule(IGNORE_RULE).unwrap();
-    git_add(A_TXT, &repo);
-    git_add(E_TXT, &repo);
-    git_add(DOTGITIGNORE, &repo);
-    git_commit("Initial commit", &repo);
-
-    let tree = Worktree::local(
-        root.path(),
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-    let root_path = root.path();
-
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    // Check that the right git state is observed on startup
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo_entry.work_directory_abs_path,
-            root_path.join("project")
-        );
-
-        assert_eq!(
-            repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
-            FileStatus::Untracked,
-        );
-        assert_eq!(
-            repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
-            FileStatus::Untracked,
-        );
-    });
-
-    // Modify a file in the working copy.
-    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    // The worktree detects that the file's git status has changed.
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo_entry.status_for_path(&A_TXT.into()).unwrap().status,
-            StatusCode::Modified.worktree(),
-        );
-    });
-
-    // Create a commit in the git repository.
-    git_add(A_TXT, &repo);
-    git_add(B_TXT, &repo);
-    git_commit("Committing modified and added", &repo);
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    // The worktree detects that the files' git status have changed.
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
-            FileStatus::Untracked,
-        );
-        assert_eq!(repo_entry.status_for_path(&B_TXT.into()), None);
-        assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
-    });
-
-    // Modify files in the working copy and perform git operations on other files.
-    git_reset(0, &repo);
-    git_remove_index(Path::new(B_TXT), &repo);
-    git_stash(&mut repo);
-    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
-    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    // Check that more complex repo changes are tracked
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-
-        assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
-        assert_eq!(
-            repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
-            FileStatus::Untracked,
-        );
-        assert_eq!(
-            repo_entry.status_for_path(&E_TXT.into()).unwrap().status,
-            StatusCode::Modified.worktree(),
-        );
-    });
-
-    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
-    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
-    std::fs::write(
-        work_dir.join(DOTGITIGNORE),
-        [IGNORE_RULE, "f.txt"].join("\n"),
-    )
-    .unwrap();
-
-    git_add(Path::new(DOTGITIGNORE), &repo);
-    git_commit("Committing modified git ignore", &repo);
-
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    let mut renamed_dir_name = "first_directory/second_directory";
-    const RENAMED_FILE: &str = "rf.txt";
-
-    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
-    std::fs::write(
-        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
-        "new-contents",
-    )
-    .unwrap();
-
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo_entry
-                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
-                .unwrap()
-                .status,
-            FileStatus::Untracked,
-        );
-    });
-
-    renamed_dir_name = "new_first_directory/second_directory";
-
-    std::fs::rename(
-        work_dir.join("first_directory"),
-        work_dir.join("new_first_directory"),
-    )
-    .unwrap();
-
-    tree.flush_fs_events(cx).await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo_entry = snapshot.repositories.iter().next().unwrap();
-
-        assert_eq!(
-            repo_entry
-                .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
-                .unwrap()
-                .status,
-            FileStatus::Untracked,
-        );
-    });
-}
-
-#[gpui::test]
-async fn test_git_repository_status(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-
-    let root = TempTree::new(json!({
-        "project": {
-            "a.txt": "a",    // Modified
-            "b.txt": "bb",   // Added
-            "c.txt": "ccc",  // Unchanged
-            "d.txt": "dddd", // Deleted
-        },
-
-    }));
-
-    // Set up git repository before creating the worktree.
-    let work_dir = root.path().join("project");
-    let repo = git_init(work_dir.as_path());
-    git_add("a.txt", &repo);
-    git_add("c.txt", &repo);
-    git_add("d.txt", &repo);
-    git_commit("Initial commit", &repo);
-    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
-    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
-
-    let tree = Worktree::local(
-        root.path(),
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    // Check that the right git state is observed on startup
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        let repo = snapshot.repositories.iter().next().unwrap();
-        let entries = repo.status().collect::<Vec<_>>();
-
-        assert_eq!(
-            entries,
-            [
-                StatusEntry {
-                    repo_path: "a.txt".into(),
-                    status: StatusCode::Modified.worktree(),
-                },
-                StatusEntry {
-                    repo_path: "b.txt".into(),
-                    status: FileStatus::Untracked,
-                },
-                StatusEntry {
-                    repo_path: "d.txt".into(),
-                    status: StatusCode::Deleted.worktree(),
-                },
-            ]
-        );
-    });
-
-    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
-
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        let repository = snapshot.repositories.iter().next().unwrap();
-        let entries = repository.status().collect::<Vec<_>>();
-
-        assert_eq!(
-            entries,
-            [
-                StatusEntry {
-                    repo_path: "a.txt".into(),
-                    status: StatusCode::Modified.worktree(),
-                },
-                StatusEntry {
-                    repo_path: "b.txt".into(),
-                    status: FileStatus::Untracked,
-                },
-                StatusEntry {
-                    repo_path: "c.txt".into(),
-                    status: StatusCode::Modified.worktree(),
-                },
-                StatusEntry {
-                    repo_path: "d.txt".into(),
-                    status: StatusCode::Deleted.worktree(),
-                },
-            ]
-        );
-    });
-
-    git_add("a.txt", &repo);
-    git_add("c.txt", &repo);
-    git_remove_index(Path::new("d.txt"), &repo);
-    git_commit("Another commit", &repo);
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
-    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        let repo = snapshot.repositories.iter().next().unwrap();
-        let entries = repo.status().collect::<Vec<_>>();
-
-        // Deleting an untracked entry, b.txt, should leave no status
-        // a.txt was tracked, and so should have a status
-        assert_eq!(
-            entries,
-            [StatusEntry {
-                repo_path: "a.txt".into(),
-                status: StatusCode::Deleted.worktree(),
-            }]
-        );
-    });
-}
-
-#[gpui::test]
-async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-
-    let root = TempTree::new(json!({
-        "project": {
-            "sub": {},
-            "a.txt": "",
-        },
-    }));
-
-    let work_dir = root.path().join("project");
-    let repo = git_init(work_dir.as_path());
-    // a.txt exists in HEAD and the working copy but is deleted in the index.
-    git_add("a.txt", &repo);
-    git_commit("Initial commit", &repo);
-    git_remove_index("a.txt".as_ref(), &repo);
-    // `sub` is a nested git repository.
-    let _sub = git_init(&work_dir.join("sub"));
-
-    let tree = Worktree::local(
-        root.path(),
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    tree.flush_fs_events(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        let repo = snapshot.repositories.iter().next().unwrap();
-        let entries = repo.status().collect::<Vec<_>>();
-
-        // `sub` doesn't appear in our computed statuses.
-        // a.txt appears with a combined `DA` status.
-        assert_eq!(
-            entries,
-            [StatusEntry {
-                repo_path: "a.txt".into(),
-                status: TrackedStatus {
-                    index_status: StatusCode::Deleted,
-                    worktree_status: StatusCode::Added
-                }
-                .into(),
-            }]
-        )
-    });
-}
-
-#[gpui::test]
-async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-
-    let root = TempTree::new(json!({
-        "my-repo": {
-            // .git folder will go here
-            "a.txt": "a",
-            "sub-folder-1": {
-                "sub-folder-2": {
-                    "c.txt": "cc",
-                    "d": {
-                        "e.txt": "eee"
-                    }
-                },
-            }
-        },
-
-    }));
-
-    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
-    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
-
-    // Set up git repository before creating the worktree.
-    let git_repo_work_dir = root.path().join("my-repo");
-    let repo = git_init(git_repo_work_dir.as_path());
-    git_add(C_TXT, &repo);
-    git_commit("Initial commit", &repo);
-
-    // Open the worktree in subfolder
-    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
-    let tree = Worktree::local(
-        root.path().join(project_root),
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    tree.flush_fs_events(cx).await;
-    tree.flush_fs_events_in_root_git_repository(cx).await;
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-    cx.executor().run_until_parked();
-
-    // Ensure that the git status is loaded correctly
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        assert_eq!(snapshot.repositories.iter().count(), 1);
-        let repo = snapshot.repositories.iter().next().unwrap();
-        assert_eq!(
-            repo.work_directory_abs_path.canonicalize().unwrap(),
-            root.path().join("my-repo").canonicalize().unwrap()
-        );
-
-        assert_eq!(repo.status_for_path(&C_TXT.into()), None);
-        assert_eq!(
-            repo.status_for_path(&E_TXT.into()).unwrap().status,
-            FileStatus::Untracked
-        );
-    });
-
-    // Now we simulate FS events, but ONLY in the .git folder that's outside
-    // of out project root.
-    // Meaning: we don't produce any FS events for files inside the project.
-    git_add(E_TXT, &repo);
-    git_commit("Second commit", &repo);
-    tree.flush_fs_events_in_root_git_repository(cx).await;
-    cx.executor().run_until_parked();
-
-    tree.read_with(cx, |tree, _cx| {
-        let snapshot = tree.snapshot();
-        let repos = snapshot.repositories().iter().cloned().collect::<Vec<_>>();
-        assert_eq!(repos.len(), 1);
-        let repo_entry = repos.into_iter().next().unwrap();
-
-        assert!(snapshot.repositories.iter().next().is_some());
-
-        assert_eq!(repo_entry.status_for_path(&C_TXT.into()), None);
-        assert_eq!(repo_entry.status_for_path(&E_TXT.into()), None);
-    });
-}
-
-#[gpui::test]
-async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
-    init_test(cx);
-    cx.executor().allow_parking();
-
-    let root = TempTree::new(json!({
-        "project": {
-            "a.txt": "a",
-        },
-    }));
-    let root_path = root.path();
-
-    let tree = Worktree::local(
-        root_path,
-        true,
-        Arc::new(RealFs::new(None, cx.executor())),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    let repo = git_init(&root_path.join("project"));
-    git_add("a.txt", &repo);
-    git_commit("init", &repo);
-
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
-    tree.flush_fs_events(cx).await;
-
-    git_branch("other-branch", &repo);
-    git_checkout("refs/heads/other-branch", &repo);
-    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
-    git_add("a.txt", &repo);
-    git_commit("capitalize", &repo);
-    let commit = repo
-        .head()
-        .expect("Failed to get HEAD")
-        .peel_to_commit()
-        .expect("HEAD is not a commit");
-    git_checkout("refs/heads/main", &repo);
-    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
-    git_add("a.txt", &repo);
-    git_commit("improve letter", &repo);
-    git_cherry_pick(&commit, &repo);
-    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
-        .expect("No CHERRY_PICK_HEAD");
-    pretty_assertions::assert_eq!(
-        git_status(&repo),
-        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
-    );
-    tree.flush_fs_events(cx).await;
-    let conflicts = tree.update(cx, |tree, _| {
-        let entry = tree.repositories.first().expect("No git entry").clone();
-        entry
-            .current_merge_conflicts
-            .iter()
-            .cloned()
-            .collect::<Vec<_>>()
-    });
-    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
-
-    git_add("a.txt", &repo);
-    // Attempt to manually simulate what `git cherry-pick --continue` would do.
-    git_commit("whatevs", &repo);
-    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
-        .expect("Failed to remove CHERRY_PICK_HEAD");
-    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
-    tree.flush_fs_events(cx).await;
-    let conflicts = tree.update(cx, |tree, _| {
-        let entry = tree.repositories.first().expect("No git entry").clone();
-        entry
-            .current_merge_conflicts
-            .iter()
-            .cloned()
-            .collect::<Vec<_>>()
-    });
-    pretty_assertions::assert_eq!(conflicts, []);
-}
-
 #[gpui::test]
 async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
     init_test(cx);
@@ -2815,110 +1984,6 @@ fn test_unrelativize() {
     );
 }
 
-#[track_caller]
-fn git_init(path: &Path) -> git2::Repository {
-    let mut init_opts = RepositoryInitOptions::new();
-    init_opts.initial_head("main");
-    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
-}
-
-#[track_caller]
-fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
-    let path = path.as_ref();
-    let mut index = repo.index().expect("Failed to get index");
-    index.add_path(path).expect("Failed to add file");
-    index.write().expect("Failed to write index");
-}
-
-#[track_caller]
-fn git_remove_index(path: &Path, repo: &git2::Repository) {
-    let mut index = repo.index().expect("Failed to get index");
-    index.remove_path(path).expect("Failed to add file");
-    index.write().expect("Failed to write index");
-}
-
-#[track_caller]
-fn git_commit(msg: &'static str, repo: &git2::Repository) {
-    use git2::Signature;
-
-    let signature = Signature::now("test", "test@zed.dev").unwrap();
-    let oid = repo.index().unwrap().write_tree().unwrap();
-    let tree = repo.find_tree(oid).unwrap();
-    if let Ok(head) = repo.head() {
-        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
-
-        let parent_commit = parent_obj.as_commit().unwrap();
-
-        repo.commit(
-            Some("HEAD"),
-            &signature,
-            &signature,
-            msg,
-            &tree,
-            &[parent_commit],
-        )
-        .expect("Failed to commit with parent");
-    } else {
-        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
-            .expect("Failed to commit");
-    }
-}
-
-#[track_caller]
-fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
-    repo.cherrypick(commit, None).expect("Failed to cherrypick");
-}
-
-#[track_caller]
-fn git_stash(repo: &mut git2::Repository) {
-    use git2::Signature;
-
-    let signature = Signature::now("test", "test@zed.dev").unwrap();
-    repo.stash_save(&signature, "N/A", None)
-        .expect("Failed to stash");
-}
-
-#[track_caller]
-fn git_reset(offset: usize, repo: &git2::Repository) {
-    let head = repo.head().expect("Couldn't get repo head");
-    let object = head.peel(git2::ObjectType::Commit).unwrap();
-    let commit = object.as_commit().unwrap();
-    let new_head = commit
-        .parents()
-        .inspect(|parnet| {
-            parnet.message();
-        })
-        .nth(offset)
-        .expect("Not enough history");
-    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
-        .expect("Could not reset");
-}
-
-#[track_caller]
-fn git_branch(name: &str, repo: &git2::Repository) {
-    let head = repo
-        .head()
-        .expect("Couldn't get repo head")
-        .peel_to_commit()
-        .expect("HEAD is not a commit");
-    repo.branch(name, &head, false).expect("Failed to commit");
-}
-
-#[track_caller]
-fn git_checkout(name: &str, repo: &git2::Repository) {
-    repo.set_head(name).expect("Failed to set head");
-    repo.checkout_head(None).expect("Failed to check out head");
-}
-
-#[track_caller]
-fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
-    repo.statuses(None)
-        .unwrap()
-        .iter()
-        .map(|status| (status.path().unwrap().to_string(), status.status()))
-        .collect()
-}
-
 #[track_caller]
 fn check_worktree_entries(
     tree: &Worktree,
@@ -2974,34 +2039,3 @@ fn init_test(cx: &mut gpui::TestAppContext) {
         WorktreeSettings::register(cx);
     });
 }
-
-#[track_caller]
-fn assert_entry_git_state(
-    tree: &Worktree,
-    path: &str,
-    index_status: Option<StatusCode>,
-    is_ignored: bool,
-) {
-    let entry = tree.entry_for_path(path).expect("entry {path} not found");
-    let repos = tree.repositories().iter().cloned().collect::<Vec<_>>();
-    assert_eq!(repos.len(), 1);
-    let repo_entry = repos.into_iter().next().unwrap();
-    let status = repo_entry
-        .status_for_path(&path.into())
-        .map(|entry| entry.status);
-    let expected = index_status.map(|index_status| {
-        TrackedStatus {
-            index_status,
-            worktree_status: StatusCode::Unmodified,
-        }
-        .into()
-    });
-    assert_eq!(
-        status, expected,
-        "expected {path} to have git status: {expected:?}"
-    );
-    assert_eq!(
-        entry.is_ignored, is_ignored,
-        "expected {path} to have is_ignored: {is_ignored}"
-    );
-}