Detailed changes
@@ -29,6 +29,12 @@ impl std::fmt::Display for ChannelId {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct ProjectId(pub u64);
+impl ProjectId {
+ pub fn to_proto(&self) -> u64 {
+ self.0
+ }
+}
+
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
@@ -1337,7 +1337,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let host_diff_base = host_project.read_with(host_cx, |project, cx| {
project
- .buffer_store()
+ .git_store()
.read(cx)
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
.unwrap()
@@ -1346,7 +1346,7 @@ impl RandomizedTest for ProjectCollaborationTest {
});
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
project
- .buffer_store()
+ .git_store()
.read(cx)
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
.unwrap()
@@ -52,7 +52,7 @@ use util::ResultExt;
#[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
-use git::repository::FakeGitRepositoryState;
+use git::FakeGitRepositoryState;
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
#[cfg(any(test, feature = "test-support"))]
@@ -885,7 +885,7 @@ enum FakeFsEntry {
mtime: MTime,
len: u64,
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
- git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
+ git_repo_state: Option<Arc<Mutex<git::FakeGitRepositoryState>>>,
},
Symlink {
target: PathBuf,
@@ -2095,7 +2095,7 @@ impl Fs for FakeFs {
)))
})
.clone();
- Some(git::repository::FakeGitRepository::open(state))
+ Some(git::FakeGitRepository::open(state))
} else {
None
}
@@ -42,3 +42,4 @@ pretty_assertions.workspace = true
serde_json.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true
+gpui = { workspace = true, features = ["test-support"] }
@@ -0,0 +1,294 @@
+use crate::{
+ blame::Blame,
+ repository::{
+ Branch, CommitDetails, DiffType, GitRepository, PushOptions, Remote, RemoteCommandOutput,
+ RepoPath, ResetMode,
+ },
+ status::{FileStatus, GitStatus},
+};
+use anyhow::{Context, Result};
+use askpass::AskPassSession;
+use collections::{HashMap, HashSet};
+use futures::{future::BoxFuture, FutureExt as _};
+use gpui::{AsyncApp, SharedString};
+use parking_lot::Mutex;
+use rope::Rope;
+use std::{path::PathBuf, sync::Arc};
+
+#[derive(Debug, Clone)]
+pub struct FakeGitRepository {
+ state: Arc<Mutex<FakeGitRepositoryState>>,
+}
+
+#[derive(Debug, Clone)]
+pub struct FakeGitRepositoryState {
+ pub path: PathBuf,
+ pub event_emitter: smol::channel::Sender<PathBuf>,
+ pub head_contents: HashMap<RepoPath, String>,
+ pub index_contents: HashMap<RepoPath, String>,
+ pub blames: HashMap<RepoPath, Blame>,
+ pub statuses: HashMap<RepoPath, FileStatus>,
+ pub current_branch_name: Option<String>,
+ pub branches: HashSet<String>,
+ pub simulated_index_write_error_message: Option<String>,
+}
+
+impl FakeGitRepository {
+ pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
+ Arc::new(FakeGitRepository { state })
+ }
+}
+
+impl FakeGitRepositoryState {
+ pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
+ FakeGitRepositoryState {
+ path,
+ event_emitter,
+ head_contents: Default::default(),
+ index_contents: Default::default(),
+ blames: Default::default(),
+ statuses: Default::default(),
+ current_branch_name: Default::default(),
+ branches: Default::default(),
+ simulated_index_write_error_message: None,
+ }
+ }
+}
+
+impl GitRepository for FakeGitRepository {
+ fn reload_index(&self) {}
+
+ fn load_index_text(&self, path: RepoPath, _: AsyncApp) -> BoxFuture<Option<String>> {
+ let state = self.state.lock();
+ let content = state.index_contents.get(path.as_ref()).cloned();
+ async { content }.boxed()
+ }
+
+ fn load_committed_text(&self, path: RepoPath, _: AsyncApp) -> BoxFuture<Option<String>> {
+ let state = self.state.lock();
+ let content = state.head_contents.get(path.as_ref()).cloned();
+ async { content }.boxed()
+ }
+
+ fn set_index_text(
+ &self,
+ path: RepoPath,
+ content: Option<String>,
+ _env: HashMap<String, String>,
+ cx: AsyncApp,
+ ) -> BoxFuture<anyhow::Result<()>> {
+ let state = self.state.clone();
+ let executor = cx.background_executor().clone();
+ async move {
+ executor.simulate_random_delay().await;
+
+ let mut state = state.lock();
+ if let Some(message) = state.simulated_index_write_error_message.clone() {
+ return Err(anyhow::anyhow!(message));
+ }
+
+ if let Some(content) = content {
+ state.index_contents.insert(path.clone(), content);
+ } else {
+ state.index_contents.remove(&path);
+ }
+ state
+ .event_emitter
+ .try_send(state.path.clone())
+ .expect("Dropped repo change event");
+
+ Ok(())
+ }
+ .boxed()
+ }
+
+ fn remote_url(&self, _name: &str) -> Option<String> {
+ None
+ }
+
+ fn head_sha(&self) -> Option<String> {
+ None
+ }
+
+ fn merge_head_shas(&self) -> Vec<String> {
+ vec![]
+ }
+
+ fn show(&self, _: String, _: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
+ unimplemented!()
+ }
+
+ fn reset(&self, _: String, _: ResetMode, _: HashMap<String, String>) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn checkout_files(
+ &self,
+ _: String,
+ _: Vec<RepoPath>,
+ _: HashMap<String, String>,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn path(&self) -> PathBuf {
+ let state = self.state.lock();
+ state.path.clone()
+ }
+
+ fn main_repository_path(&self) -> PathBuf {
+ self.path()
+ }
+
+ fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
+ let state = self.state.lock();
+
+ let mut entries = state
+ .statuses
+ .iter()
+ .filter_map(|(repo_path, status)| {
+ if path_prefixes
+ .iter()
+ .any(|path_prefix| repo_path.0.starts_with(path_prefix))
+ {
+ Some((repo_path.to_owned(), *status))
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
+
+ Ok(GitStatus {
+ entries: entries.into(),
+ })
+ }
+
+ fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
+ let state = self.state.lock();
+ let current_branch = &state.current_branch_name;
+ let result = Ok(state
+ .branches
+ .iter()
+ .map(|branch_name| Branch {
+ is_head: Some(branch_name) == current_branch.as_ref(),
+ name: branch_name.into(),
+ most_recent_commit: None,
+ upstream: None,
+ })
+ .collect());
+
+ async { result }.boxed()
+ }
+
+ fn change_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
+ let mut state = self.state.lock();
+ state.current_branch_name = Some(name.to_owned());
+ state
+ .event_emitter
+ .try_send(state.path.clone())
+ .expect("Dropped repo change event");
+ async { Ok(()) }.boxed()
+ }
+
+ fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
+ let mut state = self.state.lock();
+ state.branches.insert(name.to_owned());
+ state
+ .event_emitter
+ .try_send(state.path.clone())
+ .expect("Dropped repo change event");
+ async { Ok(()) }.boxed()
+ }
+
+ fn blame(
+ &self,
+ path: RepoPath,
+ _content: Rope,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<crate::blame::Blame>> {
+ let state = self.state.lock();
+ let result = state
+ .blames
+ .get(&path)
+ .with_context(|| format!("failed to get blame for {:?}", path.0))
+ .cloned();
+ async { result }.boxed()
+ }
+
+ fn stage_paths(
+ &self,
+ _paths: Vec<RepoPath>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn unstage_paths(
+ &self,
+ _paths: Vec<RepoPath>,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn commit(
+ &self,
+ _message: SharedString,
+ _name_and_email: Option<(SharedString, SharedString)>,
+ _env: HashMap<String, String>,
+ _: AsyncApp,
+ ) -> BoxFuture<Result<()>> {
+ unimplemented!()
+ }
+
+ fn push(
+ &self,
+ _branch: String,
+ _remote: String,
+ _options: Option<PushOptions>,
+ _ask_pass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn pull(
+ &self,
+ _branch: String,
+ _remote: String,
+ _ask_pass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn fetch(
+ &self,
+ _ask_pass: AskPassSession,
+ _env: HashMap<String, String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<RemoteCommandOutput>> {
+ unimplemented!()
+ }
+
+ fn get_remotes(
+ &self,
+ _branch: Option<String>,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<Vec<Remote>>> {
+ unimplemented!()
+ }
+
+ fn check_for_pushed_commit(&self, _cx: AsyncApp) -> BoxFuture<Result<Vec<SharedString>>> {
+ unimplemented!()
+ }
+
+ fn diff(&self, _diff: DiffType, _cx: AsyncApp) -> BoxFuture<Result<String>> {
+ unimplemented!()
+ }
+}
@@ -5,20 +5,25 @@ mod remote;
pub mod repository;
pub mod status;
+#[cfg(any(test, feature = "test-support"))]
+mod fake_repository;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use fake_repository::*;
+
+pub use crate::hosting_provider::*;
+pub use crate::remote::*;
use anyhow::{anyhow, Context as _, Result};
+pub use git2 as libgit;
use gpui::action_with_deprecated_aliases;
use gpui::actions;
+pub use repository::WORK_DIRECTORY_REPO_PATH;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
-pub use crate::hosting_provider::*;
-pub use crate::remote::*;
-pub use git2 as libgit;
-pub use repository::WORK_DIRECTORY_REPO_PATH;
-
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
pub static FSMONITOR_DAEMON: LazyLock<&'static OsStr> =
@@ -1,9 +1,8 @@
-use crate::status::FileStatus;
+use crate::status::GitStatus;
use crate::SHORT_SHA_LENGTH;
-use crate::{blame::Blame, status::GitStatus};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Context as _, Result};
use askpass::{AskPassResult, AskPassSession};
-use collections::{HashMap, HashSet};
+use collections::HashMap;
use futures::future::BoxFuture;
use futures::{select_biased, AsyncWriteExt, FutureExt as _};
use git2::BranchType;
@@ -13,11 +12,12 @@ use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
+use std::path::Component;
use std::process::Stdio;
use std::sync::LazyLock;
use std::{
cmp::Ordering,
- path::{Component, Path, PathBuf},
+ path::{Path, PathBuf},
sync::Arc,
};
use sum_tree::MapSeekTarget;
@@ -1056,304 +1056,6 @@ async fn run_remote_command(
}
}
-#[derive(Debug, Clone)]
-pub struct FakeGitRepository {
- state: Arc<Mutex<FakeGitRepositoryState>>,
-}
-
-#[derive(Debug, Clone)]
-pub struct FakeGitRepositoryState {
- pub path: PathBuf,
- pub event_emitter: smol::channel::Sender<PathBuf>,
- pub head_contents: HashMap<RepoPath, String>,
- pub index_contents: HashMap<RepoPath, String>,
- pub blames: HashMap<RepoPath, Blame>,
- pub statuses: HashMap<RepoPath, FileStatus>,
- pub current_branch_name: Option<String>,
- pub branches: HashSet<String>,
- pub simulated_index_write_error_message: Option<String>,
-}
-
-impl FakeGitRepository {
- pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
- Arc::new(FakeGitRepository { state })
- }
-}
-
-impl FakeGitRepositoryState {
- pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
- FakeGitRepositoryState {
- path,
- event_emitter,
- head_contents: Default::default(),
- index_contents: Default::default(),
- blames: Default::default(),
- statuses: Default::default(),
- current_branch_name: Default::default(),
- branches: Default::default(),
- simulated_index_write_error_message: None,
- }
- }
-}
-
-impl GitRepository for FakeGitRepository {
- fn reload_index(&self) {}
-
- fn load_index_text(&self, path: RepoPath, _: AsyncApp) -> BoxFuture<Option<String>> {
- let state = self.state.lock();
- let content = state.index_contents.get(path.as_ref()).cloned();
- async { content }.boxed()
- }
-
- fn load_committed_text(&self, path: RepoPath, _: AsyncApp) -> BoxFuture<Option<String>> {
- let state = self.state.lock();
- let content = state.head_contents.get(path.as_ref()).cloned();
- async { content }.boxed()
- }
-
- fn set_index_text(
- &self,
- path: RepoPath,
- content: Option<String>,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<anyhow::Result<()>> {
- let mut state = self.state.lock();
- if let Some(message) = state.simulated_index_write_error_message.clone() {
- return async { Err(anyhow::anyhow!(message)) }.boxed();
- }
- if let Some(content) = content {
- state.index_contents.insert(path.clone(), content);
- } else {
- state.index_contents.remove(&path);
- }
- state
- .event_emitter
- .try_send(state.path.clone())
- .expect("Dropped repo change event");
- async { Ok(()) }.boxed()
- }
-
- fn remote_url(&self, _name: &str) -> Option<String> {
- None
- }
-
- fn head_sha(&self) -> Option<String> {
- None
- }
-
- fn merge_head_shas(&self) -> Vec<String> {
- vec![]
- }
-
- fn show(&self, _: String, _: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
- unimplemented!()
- }
-
- fn reset(&self, _: String, _: ResetMode, _: HashMap<String, String>) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn checkout_files(
- &self,
- _: String,
- _: Vec<RepoPath>,
- _: HashMap<String, String>,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn path(&self) -> PathBuf {
- let state = self.state.lock();
- state.path.clone()
- }
-
- fn main_repository_path(&self) -> PathBuf {
- self.path()
- }
-
- fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
- let state = self.state.lock();
-
- let mut entries = state
- .statuses
- .iter()
- .filter_map(|(repo_path, status)| {
- if path_prefixes
- .iter()
- .any(|path_prefix| repo_path.0.starts_with(path_prefix))
- {
- Some((repo_path.to_owned(), *status))
- } else {
- None
- }
- })
- .collect::<Vec<_>>();
- entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
-
- Ok(GitStatus {
- entries: entries.into(),
- })
- }
-
- fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
- let state = self.state.lock();
- let current_branch = &state.current_branch_name;
- let result = Ok(state
- .branches
- .iter()
- .map(|branch_name| Branch {
- is_head: Some(branch_name) == current_branch.as_ref(),
- name: branch_name.into(),
- most_recent_commit: None,
- upstream: None,
- })
- .collect());
-
- async { result }.boxed()
- }
-
- fn change_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
- let mut state = self.state.lock();
- state.current_branch_name = Some(name.to_owned());
- state
- .event_emitter
- .try_send(state.path.clone())
- .expect("Dropped repo change event");
- async { Ok(()) }.boxed()
- }
-
- fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
- let mut state = self.state.lock();
- state.branches.insert(name.to_owned());
- state
- .event_emitter
- .try_send(state.path.clone())
- .expect("Dropped repo change event");
- async { Ok(()) }.boxed()
- }
-
- fn blame(
- &self,
- path: RepoPath,
- _content: Rope,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<crate::blame::Blame>> {
- let state = self.state.lock();
- let result = state
- .blames
- .get(&path)
- .with_context(|| format!("failed to get blame for {:?}", path.0))
- .cloned();
- async { result }.boxed()
- }
-
- fn stage_paths(
- &self,
- _paths: Vec<RepoPath>,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn unstage_paths(
- &self,
- _paths: Vec<RepoPath>,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn commit(
- &self,
- _message: SharedString,
- _name_and_email: Option<(SharedString, SharedString)>,
- _env: HashMap<String, String>,
- _: AsyncApp,
- ) -> BoxFuture<Result<()>> {
- unimplemented!()
- }
-
- fn push(
- &self,
- _branch: String,
- _remote: String,
- _options: Option<PushOptions>,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn pull(
- &self,
- _branch: String,
- _remote: String,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn fetch(
- &self,
- _ask_pass: AskPassSession,
- _env: HashMap<String, String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>> {
- unimplemented!()
- }
-
- fn get_remotes(
- &self,
- _branch: Option<String>,
- _cx: AsyncApp,
- ) -> BoxFuture<Result<Vec<Remote>>> {
- unimplemented!()
- }
-
- fn check_for_pushed_commit(&self, _cx: AsyncApp) -> BoxFuture<Result<Vec<SharedString>>> {
- unimplemented!()
- }
-
- fn diff(&self, _diff: DiffType, _cx: AsyncApp) -> BoxFuture<Result<String>> {
- unimplemented!()
- }
-}
-
-fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
- match relative_file_path.components().next() {
- None => anyhow::bail!("repo path should not be empty"),
- Some(Component::Prefix(_)) => anyhow::bail!(
- "repo path `{}` should be relative, not a windows prefix",
- relative_file_path.to_string_lossy()
- ),
- Some(Component::RootDir) => {
- anyhow::bail!(
- "repo path `{}` should be relative",
- relative_file_path.to_string_lossy()
- )
- }
- Some(Component::CurDir) => {
- anyhow::bail!(
- "repo path `{}` should not start with `.`",
- relative_file_path.to_string_lossy()
- )
- }
- Some(Component::ParentDir) => {
- anyhow::bail!(
- "repo path `{}` should not start with `..`",
- relative_file_path.to_string_lossy()
- )
- }
- _ => Ok(()),
- }
-}
-
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
LazyLock::new(|| RepoPath(Path::new("").into()));
@@ -1526,6 +1228,35 @@ fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
}))
}
+fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
+ match relative_file_path.components().next() {
+ None => anyhow::bail!("repo path should not be empty"),
+ Some(Component::Prefix(_)) => anyhow::bail!(
+ "repo path `{}` should be relative, not a windows prefix",
+ relative_file_path.to_string_lossy()
+ ),
+ Some(Component::RootDir) => {
+ anyhow::bail!(
+ "repo path `{}` should be relative",
+ relative_file_path.to_string_lossy()
+ )
+ }
+ Some(Component::CurDir) => {
+ anyhow::bail!(
+ "repo path `{}` should not start with `.`",
+ relative_file_path.to_string_lossy()
+ )
+ }
+ Some(Component::ParentDir) => {
+ anyhow::bail!(
+ "repo path `{}` should not start with `..`",
+ relative_file_path.to_string_lossy()
+ )
+ }
+ _ => Ok(()),
+ }
+}
+
#[test]
fn test_branches_parsing() {
// suppress "help: octal escapes are not supported, `\0` is always null"
@@ -6,7 +6,6 @@ use crate::{
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, bail, Context as _, Result};
-use buffer_diff::BufferDiff;
use client::Client;
use collections::{hash_map, HashMap, HashSet};
use fs::Fs;
@@ -20,7 +19,7 @@ use language::{
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
split_operations,
},
- Buffer, BufferEvent, Capability, DiskState, File as _, Language, LanguageRegistry, Operation,
+ Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation,
};
use rpc::{
proto::{self, ToProto},
@@ -38,22 +37,13 @@ use std::{
};
use text::BufferId;
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
-use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-enum DiffKind {
- Unstaged,
- Uncommitted,
-}
+use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId};
/// A set of open buffers.
pub struct BufferStore {
state: BufferStoreState,
#[allow(clippy::type_complexity)]
loading_buffers: HashMap<ProjectPath, Shared<Task<Result<Entity<Buffer>, Arc<anyhow::Error>>>>>,
- #[allow(clippy::type_complexity)]
- loading_diffs:
- HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
worktree_store: Entity<WorktreeStore>,
opened_buffers: HashMap<BufferId, OpenBuffer>,
downstream_client: Option<(AnyProtoClient, u64)>,
@@ -63,238 +53,9 @@ pub struct BufferStore {
#[derive(Hash, Eq, PartialEq, Clone)]
struct SharedBuffer {
buffer: Entity<Buffer>,
- diff: Option<Entity<BufferDiff>>,
lsp_handle: Option<OpenLspBufferHandle>,
}
-#[derive(Default)]
-struct BufferDiffState {
- unstaged_diff: Option<WeakEntity<BufferDiff>>,
- uncommitted_diff: Option<WeakEntity<BufferDiff>>,
- recalculate_diff_task: Option<Task<Result<()>>>,
- language: Option<Arc<Language>>,
- language_registry: Option<Arc<LanguageRegistry>>,
- diff_updated_futures: Vec<oneshot::Sender<()>>,
-
- head_text: Option<Arc<String>>,
- index_text: Option<Arc<String>>,
- head_changed: bool,
- index_changed: bool,
- language_changed: bool,
-}
-
-#[derive(Clone, Debug)]
-enum DiffBasesChange {
- SetIndex(Option<String>),
- SetHead(Option<String>),
- SetEach {
- index: Option<String>,
- head: Option<String>,
- },
- SetBoth(Option<String>),
-}
-
-impl BufferDiffState {
- fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
- self.language = buffer.read(cx).language().cloned();
- self.language_changed = true;
- let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
- }
-
- fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
- self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
- }
-
- fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
- self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
- }
-
- fn handle_base_texts_updated(
- &mut self,
- buffer: text::BufferSnapshot,
- message: proto::UpdateDiffBases,
- cx: &mut Context<Self>,
- ) {
- use proto::update_diff_bases::Mode;
-
- let Some(mode) = Mode::from_i32(message.mode) else {
- return;
- };
-
- let diff_bases_change = match mode {
- Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
- Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
- Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
- Mode::IndexAndHead => DiffBasesChange::SetEach {
- index: message.staged_text,
- head: message.committed_text,
- },
- };
-
- let _ = self.diff_bases_changed(buffer, diff_bases_change, cx);
- }
-
- pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
- if self.diff_updated_futures.is_empty() {
- return None;
- }
- let (tx, rx) = oneshot::channel();
- self.diff_updated_futures.push(tx);
- Some(rx)
- }
-
- fn diff_bases_changed(
- &mut self,
- buffer: text::BufferSnapshot,
- diff_bases_change: DiffBasesChange,
- cx: &mut Context<Self>,
- ) -> oneshot::Receiver<()> {
- match diff_bases_change {
- DiffBasesChange::SetIndex(index) => {
- self.index_text = index.map(|mut index| {
- text::LineEnding::normalize(&mut index);
- Arc::new(index)
- });
- self.index_changed = true;
- }
- DiffBasesChange::SetHead(head) => {
- self.head_text = head.map(|mut head| {
- text::LineEnding::normalize(&mut head);
- Arc::new(head)
- });
- self.head_changed = true;
- }
- DiffBasesChange::SetBoth(text) => {
- let text = text.map(|mut text| {
- text::LineEnding::normalize(&mut text);
- Arc::new(text)
- });
- self.head_text = text.clone();
- self.index_text = text;
- self.head_changed = true;
- self.index_changed = true;
- }
- DiffBasesChange::SetEach { index, head } => {
- self.index_text = index.map(|mut index| {
- text::LineEnding::normalize(&mut index);
- Arc::new(index)
- });
- self.index_changed = true;
- self.head_text = head.map(|mut head| {
- text::LineEnding::normalize(&mut head);
- Arc::new(head)
- });
- self.head_changed = true;
- }
- }
-
- self.recalculate_diffs(buffer, cx)
- }
-
- fn recalculate_diffs(
- &mut self,
- buffer: text::BufferSnapshot,
- cx: &mut Context<Self>,
- ) -> oneshot::Receiver<()> {
- log::debug!("recalculate diffs");
- let (tx, rx) = oneshot::channel();
- self.diff_updated_futures.push(tx);
-
- let language = self.language.clone();
- let language_registry = self.language_registry.clone();
- let unstaged_diff = self.unstaged_diff();
- let uncommitted_diff = self.uncommitted_diff();
- let head = self.head_text.clone();
- let index = self.index_text.clone();
- let index_changed = self.index_changed;
- let head_changed = self.head_changed;
- let language_changed = self.language_changed;
- let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
- (Some(index), Some(head)) => Arc::ptr_eq(index, head),
- (None, None) => true,
- _ => false,
- };
- self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
- let mut new_unstaged_diff = None;
- if let Some(unstaged_diff) = &unstaged_diff {
- new_unstaged_diff = Some(
- BufferDiff::update_diff(
- unstaged_diff.clone(),
- buffer.clone(),
- index,
- index_changed,
- language_changed,
- language.clone(),
- language_registry.clone(),
- &mut cx,
- )
- .await?,
- );
- }
-
- let mut new_uncommitted_diff = None;
- if let Some(uncommitted_diff) = &uncommitted_diff {
- new_uncommitted_diff = if index_matches_head {
- new_unstaged_diff.clone()
- } else {
- Some(
- BufferDiff::update_diff(
- uncommitted_diff.clone(),
- buffer.clone(),
- head,
- head_changed,
- language_changed,
- language.clone(),
- language_registry.clone(),
- &mut cx,
- )
- .await?,
- )
- }
- }
-
- let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
- unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
- {
- unstaged_diff.update(&mut cx, |diff, cx| {
- diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
- })?
- } else {
- None
- };
-
- if let Some((uncommitted_diff, new_uncommitted_diff)) =
- uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
- {
- uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
- uncommitted_diff.set_snapshot(
- &buffer,
- new_uncommitted_diff,
- language_changed,
- unstaged_changed_range,
- cx,
- );
- })?;
- }
-
- if let Some(this) = this.upgrade() {
- this.update(&mut cx, |this, _| {
- this.index_changed = false;
- this.head_changed = false;
- this.language_changed = false;
- for tx in this.diff_updated_futures.drain(..) {
- tx.send(()).ok();
- }
- })?;
- }
-
- Ok(())
- }));
-
- rx
- }
-}
-
enum BufferStoreState {
Local(LocalBufferStore),
Remote(RemoteBufferStore),
@@ -318,16 +79,13 @@ struct LocalBufferStore {
}
enum OpenBuffer {
- Complete {
- buffer: WeakEntity<Buffer>,
- diff_state: Entity<BufferDiffState>,
- },
+ Complete { buffer: WeakEntity<Buffer> },
Operations(Vec<Operation>),
}
pub enum BufferStoreEvent {
BufferAdded(Entity<Buffer>),
- BufferDiffAdded(Entity<BufferDiff>),
+ SharedBufferClosed(proto::PeerId, BufferId),
BufferDropped(BufferId),
BufferChangedFilePath {
buffer: Entity<Buffer>,
@@ -341,48 +99,6 @@ pub struct ProjectTransaction(pub HashMap<Entity<Buffer>, language::Transaction>
impl EventEmitter<BufferStoreEvent> for BufferStore {}
impl RemoteBufferStore {
- fn open_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Task<Result<Option<String>>> {
- let project_id = self.project_id;
- let client = self.upstream_client.clone();
- cx.background_spawn(async move {
- let response = client
- .request(proto::OpenUnstagedDiff {
- project_id,
- buffer_id: buffer_id.to_proto(),
- })
- .await?;
- Ok(response.staged_text)
- })
- }
-
- fn open_uncommitted_diff(
- &self,
- buffer_id: BufferId,
- cx: &App,
- ) -> Task<Result<DiffBasesChange>> {
- use proto::open_uncommitted_diff_response::Mode;
-
- let project_id = self.project_id;
- let client = self.upstream_client.clone();
- cx.background_spawn(async move {
- let response = client
- .request(proto::OpenUncommittedDiff {
- project_id,
- buffer_id: buffer_id.to_proto(),
- })
- .await?;
- let mode = Mode::from_i32(response.mode).ok_or_else(|| anyhow!("Invalid mode"))?;
- let bases = match mode {
- Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text),
- Mode::IndexAndHead => DiffBasesChange::SetEach {
- head: response.committed_text,
- index: response.staged_text,
- },
- };
- Ok(bases)
- })
- }
-
pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
@@ -647,41 +363,6 @@ impl RemoteBufferStore {
}
impl LocalBufferStore {
- 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))
- }
-
- fn load_staged_text(&self, buffer: &Entity<Buffer>, cx: &App) -> Task<Result<Option<String>>> {
- if let Some((worktree, path)) = self.worktree_for_buffer(buffer, cx) {
- worktree.read(cx).load_staged_file(path.as_ref(), cx)
- } else {
- return Task::ready(Err(anyhow!("no such worktree")));
- }
- }
-
- fn load_committed_text(
- &self,
- buffer: &Entity<Buffer>,
- cx: &App,
- ) -> Task<Result<Option<String>>> {
- if let Some((worktree, path)) = self.worktree_for_buffer(buffer, cx) {
- worktree.read(cx).load_committed_file(path.as_ref(), cx)
- } else {
- Task::ready(Err(anyhow!("no such worktree")))
- }
- }
-
fn save_local_buffer(
&self,
buffer_handle: Entity<Buffer>,
@@ -751,14 +432,6 @@ impl LocalBufferStore {
worktree::Event::UpdatedEntries(changes) => {
Self::local_worktree_entries_changed(this, &worktree, changes, cx);
}
- worktree::Event::UpdatedGitRepositories(updated_repos) => {
- Self::local_worktree_git_repos_changed(
- this,
- worktree.clone(),
- updated_repos,
- cx,
- )
- }
_ => {}
}
}
@@ -785,170 +458,6 @@ impl LocalBufferStore {
}
}
- fn local_worktree_git_repos_changed(
- this: &mut BufferStore,
- worktree_handle: Entity<Worktree>,
- changed_repos: &UpdatedGitRepositoriesSet,
- cx: &mut Context<BufferStore>,
- ) {
- debug_assert!(worktree_handle.read(cx).is_local());
-
- let mut diff_state_updates = Vec::new();
- for buffer in this.opened_buffers.values() {
- let OpenBuffer::Complete { buffer, diff_state } = buffer else {
- continue;
- };
- let Some(buffer) = buffer.upgrade() else {
- continue;
- };
- let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
- continue;
- };
- if file.worktree != worktree_handle {
- continue;
- }
- let diff_state = diff_state.read(cx);
- if changed_repos
- .iter()
- .any(|(work_dir, _)| file.path.starts_with(work_dir))
- {
- let has_unstaged_diff = diff_state
- .unstaged_diff
- .as_ref()
- .is_some_and(|diff| diff.is_upgradable());
- let has_uncommitted_diff = diff_state
- .uncommitted_diff
- .as_ref()
- .is_some_and(|set| set.is_upgradable());
- diff_state_updates.push((
- buffer,
- file.path.clone(),
- has_unstaged_diff.then(|| diff_state.index_text.clone()),
- has_uncommitted_diff.then(|| diff_state.head_text.clone()),
- ));
- }
- }
-
- if diff_state_updates.is_empty() {
- return;
- }
-
- cx.spawn(move |this, mut cx| async move {
- let snapshot =
- worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?;
- let diff_bases_changes_by_buffer = cx
- .spawn(async move |cx| {
- let mut results = Vec::new();
- for (buffer, path, current_index_text, current_head_text) in diff_state_updates
- {
- let Some(local_repo) = snapshot.local_repo_for_path(&path) else {
- continue;
- };
- let Some(relative_path) = local_repo.relativize(&path).ok() else {
- continue;
- };
- let index_text = if current_index_text.is_some() {
- local_repo
- .repo()
- .load_index_text(relative_path.clone(), cx.clone())
- .await
- } else {
- None
- };
- let head_text = if current_head_text.is_some() {
- local_repo
- .repo()
- .load_committed_text(relative_path, cx.clone())
- .await
- } else {
- None
- };
-
- // Avoid triggering a diff update if the base text has not changed.
- if let Some((current_index, current_head)) =
- current_index_text.as_ref().zip(current_head_text.as_ref())
- {
- if current_index.as_deref() == index_text.as_ref()
- && current_head.as_deref() == head_text.as_ref()
- {
- continue;
- }
- }
-
- let diff_bases_change =
- match (current_index_text.is_some(), current_head_text.is_some()) {
- (true, true) => Some(if index_text == head_text {
- DiffBasesChange::SetBoth(head_text)
- } else {
- DiffBasesChange::SetEach {
- index: index_text,
- head: head_text,
- }
- }),
- (true, false) => Some(DiffBasesChange::SetIndex(index_text)),
- (false, true) => Some(DiffBasesChange::SetHead(head_text)),
- (false, false) => None,
- };
-
- results.push((buffer, diff_bases_change))
- }
-
- results
- })
- .await;
-
- this.update(&mut cx, |this, cx| {
- for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
- let Some(OpenBuffer::Complete { diff_state, .. }) =
- this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
- else {
- continue;
- };
- let Some(diff_bases_change) = diff_bases_change else {
- continue;
- };
-
- diff_state.update(cx, |diff_state, cx| {
- use proto::update_diff_bases::Mode;
-
- let buffer = buffer.read(cx);
- if let Some((client, project_id)) = this.downstream_client.as_ref() {
- let buffer_id = buffer.remote_id().to_proto();
- let (staged_text, committed_text, mode) = match diff_bases_change
- .clone()
- {
- DiffBasesChange::SetIndex(index) => (index, None, Mode::IndexOnly),
- DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly),
- DiffBasesChange::SetEach { index, head } => {
- (index, head, Mode::IndexAndHead)
- }
- DiffBasesChange::SetBoth(text) => {
- (None, text, Mode::IndexMatchesHead)
- }
- };
- let message = proto::UpdateDiffBases {
- project_id: *project_id,
- buffer_id,
- staged_text,
- committed_text,
- mode: mode as i32,
- };
-
- client.send(message).log_err();
- }
-
- let _ = diff_state.diff_bases_changed(
- buffer.text_snapshot(),
- diff_bases_change,
- cx,
- );
- });
- }
- })
- })
- .detach_and_log_err(cx);
- }
-
fn local_worktree_entry_changed(
this: &mut BufferStore,
entry_id: ProjectEntryId,
@@ -1246,9 +755,6 @@ impl BufferStore {
client.add_entity_request_handler(Self::handle_blame_buffer);
client.add_entity_request_handler(Self::handle_reload_buffers);
client.add_entity_request_handler(Self::handle_get_permalink_to_line);
- client.add_entity_request_handler(Self::handle_open_unstaged_diff);
- client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
- client.add_entity_message_handler(Self::handle_update_diff_bases);
}
/// Creates a buffer store, optionally retaining its buffers.
@@ -1269,7 +775,6 @@ impl BufferStore {
opened_buffers: Default::default(),
shared_buffers: Default::default(),
loading_buffers: Default::default(),
- loading_diffs: Default::default(),
worktree_store,
}
}
@@ -1292,7 +797,6 @@ impl BufferStore {
downstream_client: None,
opened_buffers: Default::default(),
loading_buffers: Default::default(),
- loading_diffs: Default::default(),
shared_buffers: Default::default(),
worktree_store,
}
@@ -1364,198 +868,19 @@ impl BufferStore {
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
}
- pub fn open_unstaged_diff(
- &mut self,
- buffer: Entity<Buffer>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<BufferDiff>>> {
- let buffer_id = buffer.read(cx).remote_id();
- if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
- if let Some(unstaged_diff) = diff_state
- .read(cx)
- .unstaged_diff
- .as_ref()
- .and_then(|weak| weak.upgrade())
- {
- if let Some(task) =
- diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
- {
- return cx.background_executor().spawn(async move {
- task.await?;
- Ok(unstaged_diff)
- });
- }
- return Task::ready(Ok(unstaged_diff));
- }
- }
-
- 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 = match &self.state {
- BufferStoreState::Local(this) => this.load_staged_text(&buffer, cx),
- BufferStoreState::Remote(this) => this.open_unstaged_diff(buffer_id, cx),
- };
-
- entry
- .insert(
- cx.spawn(move |this, cx| async move {
- 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}")) })
- }
-
- pub fn open_uncommitted_diff(
- &mut self,
- buffer: Entity<Buffer>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<BufferDiff>>> {
- let buffer_id = buffer.read(cx).remote_id();
-
- if let Some(OpenBuffer::Complete { diff_state, .. }) = self.opened_buffers.get(&buffer_id) {
- if let Some(uncommitted_diff) = diff_state
- .read(cx)
- .uncommitted_diff
- .as_ref()
- .and_then(|weak| weak.upgrade())
- {
- if let Some(task) =
- diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
- {
- return cx.background_executor().spawn(async move {
- task.await?;
- Ok(uncommitted_diff)
- });
- }
- return Task::ready(Ok(uncommitted_diff));
- }
- }
-
- 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 = match &self.state {
- BufferStoreState::Local(this) => {
- let committed_text = this.load_committed_text(&buffer, cx);
- let staged_text = this.load_staged_text(&buffer, cx);
- cx.background_spawn(async move {
- let committed_text = committed_text.await?;
- let staged_text = staged_text.await?;
- let diff_bases_change = if committed_text == staged_text {
- DiffBasesChange::SetBoth(committed_text)
- } else {
- DiffBasesChange::SetEach {
- index: staged_text,
- head: committed_text,
- }
- };
- Ok(diff_bases_change)
- })
- }
- BufferStoreState::Remote(this) => this.open_uncommitted_diff(buffer_id, cx),
- };
-
- entry
- .insert(
- cx.spawn(move |this, cx| async move {
- 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}")) })
- }
-
- async fn open_diff_internal(
- this: WeakEntity<Self>,
- kind: DiffKind,
- texts: Result<DiffBasesChange>,
- buffer_entity: Entity<Buffer>,
- mut cx: AsyncApp,
- ) -> Result<Entity<BufferDiff>> {
- let diff_bases_change = match texts {
- Err(e) => {
- this.update(&mut cx, |this, cx| {
- let buffer = buffer_entity.read(cx);
- let buffer_id = buffer.remote_id();
- this.loading_diffs.remove(&(buffer_id, kind));
- })?;
- return Err(e);
- }
- Ok(change) => change,
- };
-
- this.update(&mut cx, |this, cx| {
- let buffer = buffer_entity.read(cx);
- let buffer_id = buffer.remote_id();
- let language = buffer.language().cloned();
- let language_registry = buffer.language_registry();
- let text_snapshot = buffer.text_snapshot();
- this.loading_diffs.remove(&(buffer_id, kind));
-
- if let Some(OpenBuffer::Complete { diff_state, .. }) =
- this.opened_buffers.get_mut(&buffer_id)
- {
- let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
- cx.emit(BufferStoreEvent::BufferDiffAdded(diff.clone()));
- diff_state.update(cx, |diff_state, cx| {
- diff_state.language = language;
- diff_state.language_registry = language_registry;
-
- match kind {
- DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
- DiffKind::Uncommitted => {
- let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
- diff
- } else {
- let unstaged_diff =
- cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
- diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
- unstaged_diff
- };
-
- diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
- diff_state.uncommitted_diff = Some(diff.downgrade())
- }
- };
-
- let rx = diff_state.diff_bases_changed(text_snapshot, diff_bases_change, cx);
-
- Ok(async move {
- rx.await.ok();
- Ok(diff)
- })
- })
- } else {
- Err(anyhow!("buffer was closed"))
- }
- })??
- .await
+ 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>>> {
@@ -1765,17 +1090,10 @@ impl BufferStore {
fn add_buffer(&mut self, buffer_entity: Entity<Buffer>, cx: &mut Context<Self>) -> Result<()> {
let buffer = buffer_entity.read(cx);
- let language = buffer.language().cloned();
- let language_registry = buffer.language_registry();
let remote_id = buffer.remote_id();
let is_remote = buffer.replica_id() != 0;
let open_buffer = OpenBuffer::Complete {
buffer: buffer_entity.downgrade(),
- diff_state: cx.new(|_| BufferDiffState {
- language,
- language_registry,
- ..Default::default()
- }),
};
let handle = cx.entity().downgrade();
@@ -1856,26 +1174,6 @@ impl BufferStore {
})
}
- pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
- if let OpenBuffer::Complete { diff_state, .. } = self.opened_buffers.get(&buffer_id)? {
- diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
- } else {
- None
- }
- }
-
- pub fn get_uncommitted_diff(
- &self,
- buffer_id: BufferId,
- cx: &App,
- ) -> Option<Entity<BufferDiff>> {
- if let OpenBuffer::Complete { diff_state, .. } = self.opened_buffers.get(&buffer_id)? {
- diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
- } else {
- None
- }
- }
-
pub fn buffer_version_info(&self, cx: &App) -> (Vec<proto::BufferVersion>, Vec<BufferId>) {
let buffers = self
.buffers()
@@ -1983,27 +1281,6 @@ impl BufferStore {
rx
}
- pub fn recalculate_buffer_diffs(
- &mut self,
- buffers: Vec<Entity<Buffer>>,
- cx: &mut Context<Self>,
- ) -> impl Future<Output = ()> {
- let mut futures = Vec::new();
- for buffer in buffers {
- if let Some(OpenBuffer::Complete { diff_state, .. }) =
- self.opened_buffers.get_mut(&buffer.read(cx).remote_id())
- {
- let buffer = buffer.read(cx).text_snapshot();
- futures.push(diff_state.update(cx, |diff_state, cx| {
- diff_state.recalculate_diffs(buffer, cx)
- }));
- }
- }
- async move {
- futures::future::join_all(futures).await;
- }
- }
-
fn on_buffer_event(
&mut self,
buffer: Entity<Buffer>,
@@ -2031,16 +1308,7 @@ impl BufferStore {
})
.log_err();
}
- BufferEvent::LanguageChanged => {
- let buffer_id = buffer.read(cx).remote_id();
- if let Some(OpenBuffer::Complete { diff_state, .. }) =
- self.opened_buffers.get(&buffer_id)
- {
- diff_state.update(cx, |diff_state, cx| {
- diff_state.buffer_language_changed(buffer, cx);
- });
- }
- }
+ BufferEvent::LanguageChanged => {}
_ => {}
}
}
@@ -2115,7 +1383,6 @@ impl BufferStore {
.entry(buffer_id)
.or_insert_with(|| SharedBuffer {
buffer: buffer.clone(),
- diff: None,
lsp_handle: None,
});
@@ -2295,9 +1562,10 @@ impl BufferStore {
) -> Result<()> {
let peer_id = envelope.sender_id;
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
- this.update(&mut cx, |this, _| {
+ this.update(&mut cx, |this, cx| {
if let Some(shared) = this.shared_buffers.get_mut(&peer_id) {
if shared.remove(&buffer_id).is_some() {
+ cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id));
if shared.is_empty() {
this.shared_buffers.remove(&peer_id);
}
@@ -2419,117 +1687,6 @@ impl BufferStore {
})
}
- pub async fn handle_open_unstaged_diff(
- this: Entity<Self>,
- request: TypedEnvelope<proto::OpenUnstagedDiff>,
- mut cx: AsyncApp,
- ) -> Result<proto::OpenUnstagedDiffResponse> {
- let buffer_id = BufferId::new(request.payload.buffer_id)?;
- let diff = this
- .update(&mut cx, |this, cx| {
- let buffer = this.get(buffer_id)?;
- Some(this.open_unstaged_diff(buffer, cx))
- })?
- .ok_or_else(|| anyhow!("no such buffer"))?
- .await?;
- this.update(&mut cx, |this, _| {
- let shared_buffers = this
- .shared_buffers
- .entry(request.original_sender_id.unwrap_or(request.sender_id))
- .or_default();
- debug_assert!(shared_buffers.contains_key(&buffer_id));
- if let Some(shared) = shared_buffers.get_mut(&buffer_id) {
- shared.diff = Some(diff.clone());
- }
- })?;
- let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?;
- Ok(proto::OpenUnstagedDiffResponse { staged_text })
- }
-
- pub async fn handle_open_uncommitted_diff(
- this: Entity<Self>,
- request: TypedEnvelope<proto::OpenUncommittedDiff>,
- mut cx: AsyncApp,
- ) -> Result<proto::OpenUncommittedDiffResponse> {
- let buffer_id = BufferId::new(request.payload.buffer_id)?;
- let diff = this
- .update(&mut cx, |this, cx| {
- let buffer = this.get(buffer_id)?;
- Some(this.open_uncommitted_diff(buffer, cx))
- })?
- .ok_or_else(|| anyhow!("no such buffer"))?
- .await?;
- this.update(&mut cx, |this, _| {
- let shared_buffers = this
- .shared_buffers
- .entry(request.original_sender_id.unwrap_or(request.sender_id))
- .or_default();
- debug_assert!(shared_buffers.contains_key(&buffer_id));
- if let Some(shared) = shared_buffers.get_mut(&buffer_id) {
- shared.diff = Some(diff.clone());
- }
- })?;
- diff.read_with(&cx, |diff, cx| {
- use proto::open_uncommitted_diff_response::Mode;
-
- let unstaged_diff = diff.secondary_diff();
- let index_snapshot = unstaged_diff.and_then(|diff| {
- let diff = diff.read(cx);
- diff.base_text_exists().then(|| diff.base_text())
- });
-
- let mode;
- let staged_text;
- let committed_text;
- if diff.base_text_exists() {
- let committed_snapshot = diff.base_text();
- committed_text = Some(committed_snapshot.text());
- if let Some(index_text) = index_snapshot {
- if index_text.remote_id() == committed_snapshot.remote_id() {
- mode = Mode::IndexMatchesHead;
- staged_text = None;
- } else {
- mode = Mode::IndexAndHead;
- staged_text = Some(index_text.text());
- }
- } else {
- mode = Mode::IndexAndHead;
- staged_text = None;
- }
- } else {
- mode = Mode::IndexAndHead;
- committed_text = None;
- staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
- }
-
- proto::OpenUncommittedDiffResponse {
- committed_text,
- staged_text,
- mode: mode.into(),
- }
- })
- }
-
- pub async fn handle_update_diff_bases(
- this: Entity<Self>,
- request: TypedEnvelope<proto::UpdateDiffBases>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- let buffer_id = BufferId::new(request.payload.buffer_id)?;
- this.update(&mut cx, |this, cx| {
- if let Some(OpenBuffer::Complete { diff_state, buffer }) =
- this.opened_buffers.get_mut(&buffer_id)
- {
- if let Some(buffer) = buffer.upgrade() {
- let buffer = buffer.read(cx).text_snapshot();
- diff_state.update(cx, |diff_state, cx| {
- diff_state.handle_base_texts_updated(buffer, request.payload, cx);
- })
- }
- }
- })
- }
-
pub fn reload_buffers(
&self,
buffers: HashSet<Entity<Buffer>>,
@@ -3,16 +3,16 @@ use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
Project, ProjectEnvironment, ProjectItem, ProjectPath,
};
-use anyhow::{Context as _, Result};
+use anyhow::{anyhow, Context as _, Result};
use askpass::{AskPassDelegate, AskPassSession};
-use buffer_diff::BufferDiffEvent;
+use buffer_diff::{BufferDiff, BufferDiffEvent};
use client::ProjectId;
use collections::HashMap;
use fs::Fs;
use futures::{
channel::{mpsc, oneshot},
- future::OptionFuture,
- StreamExt as _,
+ future::{OptionFuture, Shared},
+ FutureExt as _, StreamExt as _,
};
use git::repository::DiffType;
use git::{
@@ -26,15 +26,15 @@ use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
WeakEntity,
};
-use language::{Buffer, LanguageRegistry};
+use language::{Buffer, BufferEvent, Language, LanguageRegistry};
use parking_lot::Mutex;
use rpc::{
- proto::{self, git_reset, ToProto},
+ proto::{self, git_reset, ToProto, SSH_PROJECT_ID},
AnyProtoClient, TypedEnvelope,
};
use settings::WorktreeId;
use std::{
- collections::VecDeque,
+ collections::{hash_map, VecDeque},
future::Future,
path::{Path, PathBuf},
sync::Arc,
@@ -42,27 +42,75 @@ use std::{
use text::BufferId;
use util::{debug_panic, maybe, ResultExt};
-use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
+use worktree::{
+ File, ProjectEntryId, RepositoryEntry, StatusEntry, UpdatedGitRepositoriesSet, WorkDirectory,
+ Worktree,
+};
pub struct GitStore {
state: GitStoreState,
buffer_store: Entity<BufferStore>,
repositories: Vec<Entity<Repository>>,
+ #[allow(clippy::type_complexity)]
+ loading_diffs:
+ HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
+ diffs: HashMap<BufferId, Entity<BufferDiffState>>,
active_index: Option<usize>,
update_sender: mpsc::UnboundedSender<GitJob>,
+ shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
_subscriptions: [Subscription; 2],
}
+#[derive(Default)]
+struct SharedDiffs {
+ unstaged: Option<Entity<BufferDiff>>,
+ uncommitted: Option<Entity<BufferDiff>>,
+}
+
+#[derive(Default)]
+struct BufferDiffState {
+ unstaged_diff: Option<WeakEntity<BufferDiff>>,
+ uncommitted_diff: Option<WeakEntity<BufferDiff>>,
+ recalculate_diff_task: Option<Task<Result<()>>>,
+ language: Option<Arc<Language>>,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ diff_updated_futures: Vec<oneshot::Sender<()>>,
+
+ head_text: Option<Arc<String>>,
+ index_text: Option<Arc<String>>,
+ head_changed: bool,
+ index_changed: bool,
+ language_changed: bool,
+}
+
+#[derive(Clone, Debug)]
+enum DiffBasesChange {
+ SetIndex(Option<String>),
+ SetHead(Option<String>),
+ SetEach {
+ index: Option<String>,
+ head: Option<String>,
+ },
+ SetBoth(Option<String>),
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+enum DiffKind {
+ Unstaged,
+ Uncommitted,
+}
+
enum GitStoreState {
Local {
- client: AnyProtoClient,
+ downstream_client: Option<(AnyProtoClient, ProjectId)>,
environment: Entity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
},
Ssh {
- environment: Entity<ProjectEnvironment>,
upstream_client: AnyProtoClient,
- project_id: ProjectId,
+ upstream_project_id: ProjectId,
+ downstream_client: Option<(AnyProtoClient, ProjectId)>,
+ environment: Entity<ProjectEnvironment>,
},
Remote {
upstream_client: AnyProtoClient,
@@ -123,29 +171,18 @@ impl GitStore {
buffer_store: Entity<BufferStore>,
environment: Entity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
- client: AnyProtoClient,
- cx: &mut Context<'_, Self>,
+ cx: &mut Context<Self>,
) -> Self {
- let update_sender = Self::spawn_git_worker(cx);
- let _subscriptions = [
- cx.subscribe(worktree_store, Self::on_worktree_store_event),
- cx.subscribe(&buffer_store, Self::on_buffer_store_event),
- ];
-
- let state = GitStoreState::Local {
- client,
- environment,
- fs,
- };
-
- GitStore {
- state,
+ Self::new(
+ worktree_store,
buffer_store,
- repositories: Vec::new(),
- active_index: None,
- update_sender,
- _subscriptions,
- }
+ GitStoreState::Local {
+ downstream_client: None,
+ environment,
+ fs,
+ },
+ cx,
+ )
}
pub fn remote(
@@ -153,27 +190,17 @@ impl GitStore {
buffer_store: Entity<BufferStore>,
upstream_client: AnyProtoClient,
project_id: ProjectId,
- cx: &mut Context<'_, Self>,
+ cx: &mut Context<Self>,
) -> Self {
- let update_sender = Self::spawn_git_worker(cx);
- let _subscriptions = [
- cx.subscribe(worktree_store, Self::on_worktree_store_event),
- cx.subscribe(&buffer_store, Self::on_buffer_store_event),
- ];
-
- let state = GitStoreState::Remote {
- upstream_client,
- project_id,
- };
-
- GitStore {
- state,
+ Self::new(
+ worktree_store,
buffer_store,
- repositories: Vec::new(),
- active_index: None,
- update_sender,
- _subscriptions,
- }
+ GitStoreState::Remote {
+ upstream_client,
+ project_id,
+ },
+ cx,
+ )
}
pub fn ssh(
@@ -181,8 +208,26 @@ impl GitStore {
buffer_store: Entity<BufferStore>,
environment: Entity<ProjectEnvironment>,
upstream_client: AnyProtoClient,
- project_id: ProjectId,
- cx: &mut Context<'_, Self>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ Self::new(
+ worktree_store,
+ buffer_store,
+ GitStoreState::Ssh {
+ upstream_client,
+ upstream_project_id: ProjectId(SSH_PROJECT_ID),
+ downstream_client: None,
+ environment,
+ },
+ cx,
+ )
+ }
+
+ fn new(
+ worktree_store: &Entity<WorktreeStore>,
+ buffer_store: Entity<BufferStore>,
+ state: GitStoreState,
+ cx: &mut Context<Self>,
) -> Self {
let update_sender = Self::spawn_git_worker(cx);
let _subscriptions = [
@@ -190,12 +235,6 @@ impl GitStore {
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
];
- let state = GitStoreState::Ssh {
- upstream_client,
- project_id,
- environment,
- };
-
GitStore {
state,
buffer_store,
@@ -203,6 +242,9 @@ impl GitStore {
active_index: None,
update_sender,
_subscriptions,
+ loading_diffs: HashMap::default(),
+ shared_diffs: HashMap::default(),
+ diffs: HashMap::default(),
}
}
@@ -226,6 +268,50 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_askpass);
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
client.add_entity_request_handler(Self::handle_git_diff);
+ client.add_entity_request_handler(Self::handle_open_unstaged_diff);
+ client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
+ client.add_entity_message_handler(Self::handle_update_diff_bases);
+ }
+
+ pub fn is_local(&self) -> bool {
+ matches!(self.state, GitStoreState::Local { .. })
+ }
+
+ pub fn shared(&mut self, remote_id: u64, client: AnyProtoClient, _cx: &mut App) {
+ match &mut self.state {
+ GitStoreState::Local {
+ downstream_client, ..
+ }
+ | GitStoreState::Ssh {
+ downstream_client, ..
+ } => {
+ *downstream_client = Some((client, ProjectId(remote_id)));
+ }
+ GitStoreState::Remote { .. } => {
+ debug_panic!("shared called on remote store");
+ }
+ }
+ }
+
+ pub fn unshared(&mut self, _cx: &mut Context<Self>) {
+ match &mut self.state {
+ GitStoreState::Local {
+ downstream_client, ..
+ }
+ | GitStoreState::Ssh {
+ downstream_client, ..
+ } => {
+ downstream_client.take();
+ }
+ GitStoreState::Remote { .. } => {
+ debug_panic!("unshared called on remote store");
+ }
+ }
+ self.shared_diffs.clear();
+ }
+
+ pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) {
+ self.shared_diffs.remove(peer_id);
}
pub fn active_repository(&self) -> Option<Entity<Repository>> {
@@ -233,15 +319,213 @@ impl GitStore {
.map(|index| self.repositories[index].clone())
}
- fn client(&self) -> AnyProtoClient {
+ pub fn open_unstaged_diff(
+ &mut self,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Entity<BufferDiff>>> {
+ let buffer_id = buffer.read(cx).remote_id();
+ if let Some(diff_state) = self.diffs.get(&buffer_id) {
+ if let Some(unstaged_diff) = diff_state
+ .read(cx)
+ .unstaged_diff
+ .as_ref()
+ .and_then(|weak| weak.upgrade())
+ {
+ if let Some(task) =
+ diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
+ {
+ return cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(unstaged_diff)
+ });
+ }
+ return Task::ready(Ok(unstaged_diff));
+ }
+ }
+
+ 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(move |this, cx| async move {
+ 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}")) })
+ }
+
+ pub fn open_uncommitted_diff(
+ &mut self,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Entity<BufferDiff>>> {
+ let buffer_id = buffer.read(cx).remote_id();
+
+ if let Some(diff_state) = self.diffs.get(&buffer_id) {
+ if let Some(uncommitted_diff) = diff_state
+ .read(cx)
+ .uncommitted_diff
+ .as_ref()
+ .and_then(|weak| weak.upgrade())
+ {
+ if let Some(task) =
+ diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
+ {
+ return cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(uncommitted_diff)
+ });
+ }
+ return Task::ready(Ok(uncommitted_diff));
+ }
+ }
+
+ 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(move |this, cx| async move {
+ 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}")) })
+ }
+
+ async fn open_diff_internal(
+ this: WeakEntity<Self>,
+ kind: DiffKind,
+ texts: Result<DiffBasesChange>,
+ buffer_entity: Entity<Buffer>,
+ mut cx: AsyncApp,
+ ) -> Result<Entity<BufferDiff>> {
+ let diff_bases_change = match texts {
+ Err(e) => {
+ this.update(&mut cx, |this, cx| {
+ let buffer = buffer_entity.read(cx);
+ let buffer_id = buffer.remote_id();
+ this.loading_diffs.remove(&(buffer_id, kind));
+ })?;
+ return Err(e);
+ }
+ Ok(change) => change,
+ };
+
+ this.update(&mut cx, |this, cx| {
+ let buffer = buffer_entity.read(cx);
+ let buffer_id = buffer.remote_id();
+ let language = buffer.language().cloned();
+ let language_registry = buffer.language_registry();
+ let text_snapshot = buffer.text_snapshot();
+ this.loading_diffs.remove(&(buffer_id, kind));
+
+ let diff_state = this
+ .diffs
+ .entry(buffer_id)
+ .or_insert_with(|| cx.new(|_| BufferDiffState::default()));
+
+ let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
+
+ cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
+ diff_state.update(cx, |diff_state, cx| {
+ diff_state.language = language;
+ diff_state.language_registry = language_registry;
+
+ match kind {
+ DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
+ DiffKind::Uncommitted => {
+ let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
+ diff
+ } else {
+ let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
+ diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
+ unstaged_diff
+ };
+
+ diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
+ diff_state.uncommitted_diff = Some(diff.downgrade())
+ }
+ }
+
+ let rx = diff_state.diff_bases_changed(text_snapshot, diff_bases_change, cx);
+
+ anyhow::Ok(async move {
+ rx.await.ok();
+ Ok(diff)
+ })
+ })
+ })??
+ .await
+ }
+
+ pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
+ let diff_state = self.diffs.get(&buffer_id)?;
+ diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
+ }
+
+ pub fn get_uncommitted_diff(
+ &self,
+ buffer_id: BufferId,
+ cx: &App,
+ ) -> Option<Entity<BufferDiff>> {
+ let diff_state = self.diffs.get(&buffer_id)?;
+ diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
+ }
+
+ fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
match &self.state {
- GitStoreState::Local { client, .. } => client.clone(),
+ GitStoreState::Local {
+ downstream_client, ..
+ }
+ | GitStoreState::Ssh {
+ downstream_client, ..
+ } => downstream_client.clone(),
+ GitStoreState::Remote { .. } => None,
+ }
+ }
+
+ fn upstream_client(&self) -> Option<AnyProtoClient> {
+ match &self.state {
+ GitStoreState::Local { .. } => None,
GitStoreState::Ssh {
upstream_client, ..
- } => upstream_client.clone(),
- GitStoreState::Remote {
+ }
+ | GitStoreState::Remote {
upstream_client, ..
- } => upstream_client.clone(),
+ } => Some(upstream_client.clone()),
}
}
@@ -256,7 +540,7 @@ impl GitStore {
fn project_id(&self) -> Option<ProjectId> {
match &self.state {
GitStoreState::Local { .. } => None,
- GitStoreState::Ssh { project_id, .. } => Some(*project_id),
+ GitStoreState::Ssh { .. } => Some(ProjectId(proto::SSH_PROJECT_ID)),
GitStoreState::Remote { project_id, .. } => Some(*project_id),
}
}
@@ -265,12 +549,12 @@ impl GitStore {
&mut self,
worktree_store: Entity<WorktreeStore>,
event: &WorktreeStoreEvent,
- cx: &mut Context<'_, Self>,
+ cx: &mut Context<Self>,
) {
let mut new_repositories = Vec::new();
let mut new_active_index = None;
let this = cx.weak_entity();
- let client = self.client();
+ let upstream_client = self.upstream_client();
let project_id = self.project_id();
worktree_store.update(cx, |worktree_store, cx| {
@@ -288,7 +572,10 @@ impl GitStore {
)
})
.or_else(|| {
- let client = client.clone();
+ let client = upstream_client
+ .clone()
+ .context("no upstream client")
+ .log_err()?;
let project_id = project_id?;
Some((
GitRepo::Remote {
@@ -373,33 +660,94 @@ impl GitStore {
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_) => {
cx.emit(GitEvent::GitStateUpdated);
}
+ WorktreeStoreEvent::WorktreeAdded(worktree) => {
+ if self.is_local() {
+ cx.subscribe(worktree, Self::on_worktree_event).detach();
+ }
+ }
_ => {
cx.emit(GitEvent::FileSystemUpdated);
}
}
}
+ fn on_worktree_event(
+ &mut self,
+ worktree: Entity<Worktree>,
+ event: &worktree::Event,
+ cx: &mut Context<Self>,
+ ) {
+ if let worktree::Event::UpdatedGitRepositories(changed_repos) = event {
+ self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
+ }
+ }
+
fn on_buffer_store_event(
&mut self,
_: Entity<BufferStore>,
event: &BufferStoreEvent,
- cx: &mut Context<'_, Self>,
+ cx: &mut Context<Self>,
) {
- if let BufferStoreEvent::BufferDiffAdded(diff) = event {
- cx.subscribe(diff, Self::on_buffer_diff_event).detach();
+ match event {
+ BufferStoreEvent::BufferAdded(buffer) => {
+ cx.subscribe(&buffer, |this, buffer, event, cx| {
+ if let BufferEvent::LanguageChanged = event {
+ let buffer_id = buffer.read(cx).remote_id();
+ if let Some(diff_state) = this.diffs.get(&buffer_id) {
+ diff_state.update(cx, |diff_state, cx| {
+ diff_state.buffer_language_changed(buffer, cx);
+ });
+ }
+ }
+ })
+ .detach();
+ }
+ BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => {
+ if let Some(diffs) = self.shared_diffs.get_mut(peer_id) {
+ diffs.remove(buffer_id);
+ }
+ }
+ BufferStoreEvent::BufferDropped(buffer_id) => {
+ self.diffs.remove(&buffer_id);
+ for diffs in self.shared_diffs.values_mut() {
+ diffs.remove(buffer_id);
+ }
+ }
+
+ _ => {}
+ }
+ }
+
+ pub fn recalculate_buffer_diffs(
+ &mut self,
+ buffers: Vec<Entity<Buffer>>,
+ cx: &mut Context<Self>,
+ ) -> impl Future<Output = ()> {
+ let mut futures = Vec::new();
+ for buffer in buffers {
+ if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
+ let buffer = buffer.read(cx).text_snapshot();
+ futures.push(diff_state.update(cx, |diff_state, cx| {
+ diff_state.recalculate_diffs(buffer, cx)
+ }));
+ }
+ }
+ async move {
+ futures::future::join_all(futures).await;
}
}
fn on_buffer_diff_event(
- this: &mut GitStore,
+ &mut self,
diff: Entity<buffer_diff::BufferDiff>,
event: &BufferDiffEvent,
- cx: &mut Context<'_, GitStore>,
+ cx: &mut Context<Self>,
) {
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
let buffer_id = diff.read(cx).buffer_id;
- if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
+ if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
let recv = repo.update(cx, |repo, cx| {
+ log::debug!("updating index text for buffer {}", path.display());
repo.set_index_text(
path,
new_index_text.as_ref().map(|rope| rope.to_string()),
@@ -425,6 +773,160 @@ impl GitStore {
}
}
+ fn local_worktree_git_repos_changed(
+ &mut self,
+ worktree: Entity<Worktree>,
+ changed_repos: &UpdatedGitRepositoriesSet,
+ cx: &mut Context<Self>,
+ ) {
+ debug_assert!(worktree.read(cx).is_local());
+
+ let mut diff_state_updates = Vec::new();
+ for (buffer_id, diff_state) in &self.diffs {
+ let Some(buffer) = self.buffer_store.read(cx).get(*buffer_id) else {
+ continue;
+ };
+ let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
+ continue;
+ };
+ if file.worktree != worktree
+ || !changed_repos
+ .iter()
+ .any(|(work_dir, _)| file.path.starts_with(work_dir))
+ {
+ continue;
+ }
+
+ let diff_state = diff_state.read(cx);
+ let has_unstaged_diff = diff_state
+ .unstaged_diff
+ .as_ref()
+ .is_some_and(|diff| diff.is_upgradable());
+ let has_uncommitted_diff = diff_state
+ .uncommitted_diff
+ .as_ref()
+ .is_some_and(|set| set.is_upgradable());
+ diff_state_updates.push((
+ buffer,
+ file.path.clone(),
+ has_unstaged_diff.then(|| diff_state.index_text.clone()),
+ has_uncommitted_diff.then(|| diff_state.head_text.clone()),
+ ));
+ }
+
+ if diff_state_updates.is_empty() {
+ return;
+ }
+
+ cx.spawn(move |this, mut cx| async move {
+ let snapshot =
+ worktree.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?;
+
+ let mut diff_bases_changes_by_buffer = Vec::new();
+ for (buffer, path, current_index_text, current_head_text) in diff_state_updates {
+ log::debug!("reloading git state for buffer {}", path.display());
+ let Some(local_repo) = snapshot.local_repo_for_path(&path) else {
+ continue;
+ };
+ let Some(relative_path) = local_repo.relativize(&path).ok() else {
+ continue;
+ };
+ let index_text = if current_index_text.is_some() {
+ local_repo
+ .repo()
+ .load_index_text(relative_path.clone(), cx.clone())
+ .await
+ } else {
+ None
+ };
+ let head_text = if current_head_text.is_some() {
+ local_repo
+ .repo()
+ .load_committed_text(relative_path, cx.clone())
+ .await
+ } else {
+ None
+ };
+
+ // Avoid triggering a diff update if the base text has not changed.
+ if let Some((current_index, current_head)) =
+ current_index_text.as_ref().zip(current_head_text.as_ref())
+ {
+ if current_index.as_deref() == index_text.as_ref()
+ && current_head.as_deref() == head_text.as_ref()
+ {
+ continue;
+ }
+ }
+
+ let diff_bases_change =
+ match (current_index_text.is_some(), current_head_text.is_some()) {
+ (true, true) => Some(if index_text == head_text {
+ DiffBasesChange::SetBoth(head_text)
+ } else {
+ DiffBasesChange::SetEach {
+ index: index_text,
+ head: head_text,
+ }
+ }),
+ (true, false) => Some(DiffBasesChange::SetIndex(index_text)),
+ (false, true) => Some(DiffBasesChange::SetHead(head_text)),
+ (false, false) => None,
+ };
+
+ diff_bases_changes_by_buffer.push((buffer, diff_bases_change))
+ }
+
+ this.update(&mut cx, |this, cx| {
+ for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
+ let Some(diff_state) = this.diffs.get(&buffer.read(cx).remote_id()) else {
+ continue;
+ };
+ let Some(diff_bases_change) = diff_bases_change else {
+ continue;
+ };
+
+ let downstream_client = this.downstream_client();
+ diff_state.update(cx, |diff_state, cx| {
+ use proto::update_diff_bases::Mode;
+
+ let buffer = buffer.read(cx);
+ if let Some((client, project_id)) = downstream_client {
+ let (staged_text, committed_text, mode) = match diff_bases_change
+ .clone()
+ {
+ DiffBasesChange::SetIndex(index) => (index, None, Mode::IndexOnly),
+ DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly),
+ DiffBasesChange::SetEach { index, head } => {
+ (index, head, Mode::IndexAndHead)
+ }
+ DiffBasesChange::SetBoth(text) => {
+ (None, text, Mode::IndexMatchesHead)
+ }
+ };
+ let message = proto::UpdateDiffBases {
+ project_id: project_id.to_proto(),
+ buffer_id: buffer.remote_id().to_proto(),
+ staged_text,
+ committed_text,
+ mode: mode as i32,
+ };
+
+ client.send(message).log_err();
+ }
+
+ let _ = diff_state.diff_bases_changed(
+ buffer.text_snapshot(),
+ diff_bases_change,
+ cx,
+ );
+ });
+ }
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+
pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
self.repositories.clone()
}
@@ -459,7 +961,7 @@ impl GitStore {
result
}
- fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {
+ fn spawn_git_worker(cx: &mut Context<GitStore>) -> mpsc::UnboundedSender<GitJob> {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
cx.spawn(|_, mut cx| async move {
@@ -504,12 +1006,13 @@ impl GitStore {
}
GitStoreState::Ssh {
upstream_client,
- project_id,
+ upstream_project_id: project_id,
..
}
| GitStoreState::Remote {
upstream_client,
project_id,
+ ..
} => {
let client = upstream_client.clone();
let project_id = *project_id;
@@ -1014,6 +1517,109 @@ impl GitStore {
Ok(proto::GitDiffResponse { diff })
}
+ pub async fn handle_open_unstaged_diff(
+ this: Entity<Self>,
+ request: TypedEnvelope<proto::OpenUnstagedDiff>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::OpenUnstagedDiffResponse> {
+ let buffer_id = BufferId::new(request.payload.buffer_id)?;
+ let diff = this
+ .update(&mut cx, |this, cx| {
+ let buffer = this.buffer_store.read(cx).get(buffer_id)?;
+ Some(this.open_unstaged_diff(buffer, cx))
+ })?
+ .ok_or_else(|| anyhow!("no such buffer"))?
+ .await?;
+ this.update(&mut cx, |this, _| {
+ let shared_diffs = this
+ .shared_diffs
+ .entry(request.original_sender_id.unwrap_or(request.sender_id))
+ .or_default();
+ shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
+ })?;
+ let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?;
+ Ok(proto::OpenUnstagedDiffResponse { staged_text })
+ }
+
+ pub async fn handle_open_uncommitted_diff(
+ this: Entity<Self>,
+ request: TypedEnvelope<proto::OpenUncommittedDiff>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::OpenUncommittedDiffResponse> {
+ let buffer_id = BufferId::new(request.payload.buffer_id)?;
+ let diff = this
+ .update(&mut cx, |this, cx| {
+ let buffer = this.buffer_store.read(cx).get(buffer_id)?;
+ Some(this.open_uncommitted_diff(buffer, cx))
+ })?
+ .ok_or_else(|| anyhow!("no such buffer"))?
+ .await?;
+ this.update(&mut cx, |this, _| {
+ let shared_diffs = this
+ .shared_diffs
+ .entry(request.original_sender_id.unwrap_or(request.sender_id))
+ .or_default();
+ shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
+ })?;
+ diff.read_with(&cx, |diff, cx| {
+ use proto::open_uncommitted_diff_response::Mode;
+
+ let unstaged_diff = diff.secondary_diff();
+ let index_snapshot = unstaged_diff.and_then(|diff| {
+ let diff = diff.read(cx);
+ diff.base_text_exists().then(|| diff.base_text())
+ });
+
+ let mode;
+ let staged_text;
+ let committed_text;
+ if diff.base_text_exists() {
+ let committed_snapshot = diff.base_text();
+ committed_text = Some(committed_snapshot.text());
+ if let Some(index_text) = index_snapshot {
+ if index_text.remote_id() == committed_snapshot.remote_id() {
+ mode = Mode::IndexMatchesHead;
+ staged_text = None;
+ } else {
+ mode = Mode::IndexAndHead;
+ staged_text = Some(index_text.text());
+ }
+ } else {
+ mode = Mode::IndexAndHead;
+ staged_text = None;
+ }
+ } else {
+ mode = Mode::IndexAndHead;
+ committed_text = None;
+ staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
+ }
+
+ proto::OpenUncommittedDiffResponse {
+ committed_text,
+ staged_text,
+ mode: mode.into(),
+ }
+ })
+ }
+
+ pub async fn handle_update_diff_bases(
+ this: Entity<Self>,
+ request: TypedEnvelope<proto::UpdateDiffBases>,
+ mut cx: AsyncApp,
+ ) -> Result<()> {
+ let buffer_id = BufferId::new(request.payload.buffer_id)?;
+ this.update(&mut cx, |this, cx| {
+ if let Some(diff_state) = this.diffs.get_mut(&buffer_id) {
+ if let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) {
+ let buffer = buffer.read(cx).text_snapshot();
+ diff_state.update(cx, |diff_state, cx| {
+ diff_state.handle_base_texts_updated(buffer, request.payload, cx);
+ })
+ }
+ }
+ })
+ }
+
fn repository_for_request(
this: &Entity<Self>,
worktree_id: WorktreeId,
@@ -1037,6 +1643,207 @@ impl GitStore {
}
}
+impl BufferDiffState {
+ fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
+ self.language = buffer.read(cx).language().cloned();
+ self.language_changed = true;
+ let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
+ }
+
+ fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
+ self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
+ }
+
+ fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
+ self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
+ }
+
+ fn handle_base_texts_updated(
+ &mut self,
+ buffer: text::BufferSnapshot,
+ message: proto::UpdateDiffBases,
+ cx: &mut Context<Self>,
+ ) {
+ use proto::update_diff_bases::Mode;
+
+ let Some(mode) = Mode::from_i32(message.mode) else {
+ return;
+ };
+
+ let diff_bases_change = match mode {
+ Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
+ Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
+ Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
+ Mode::IndexAndHead => DiffBasesChange::SetEach {
+ index: message.staged_text,
+ head: message.committed_text,
+ },
+ };
+
+ let _ = self.diff_bases_changed(buffer, diff_bases_change, cx);
+ }
+
+ pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
+ if self.diff_updated_futures.is_empty() {
+ return None;
+ }
+ let (tx, rx) = oneshot::channel();
+ self.diff_updated_futures.push(tx);
+ Some(rx)
+ }
+
+ fn diff_bases_changed(
+ &mut self,
+ buffer: text::BufferSnapshot,
+ diff_bases_change: DiffBasesChange,
+ cx: &mut Context<Self>,
+ ) -> oneshot::Receiver<()> {
+ match diff_bases_change {
+ DiffBasesChange::SetIndex(index) => {
+ self.index_text = index.map(|mut index| {
+ text::LineEnding::normalize(&mut index);
+ Arc::new(index)
+ });
+ self.index_changed = true;
+ }
+ DiffBasesChange::SetHead(head) => {
+ self.head_text = head.map(|mut head| {
+ text::LineEnding::normalize(&mut head);
+ Arc::new(head)
+ });
+ self.head_changed = true;
+ }
+ DiffBasesChange::SetBoth(text) => {
+ let text = text.map(|mut text| {
+ text::LineEnding::normalize(&mut text);
+ Arc::new(text)
+ });
+ self.head_text = text.clone();
+ self.index_text = text;
+ self.head_changed = true;
+ self.index_changed = true;
+ }
+ DiffBasesChange::SetEach { index, head } => {
+ self.index_text = index.map(|mut index| {
+ text::LineEnding::normalize(&mut index);
+ Arc::new(index)
+ });
+ self.index_changed = true;
+ self.head_text = head.map(|mut head| {
+ text::LineEnding::normalize(&mut head);
+ Arc::new(head)
+ });
+ self.head_changed = true;
+ }
+ }
+
+ self.recalculate_diffs(buffer, cx)
+ }
+
+ fn recalculate_diffs(
+ &mut self,
+ buffer: text::BufferSnapshot,
+ cx: &mut Context<Self>,
+ ) -> oneshot::Receiver<()> {
+ log::debug!("recalculate diffs");
+ let (tx, rx) = oneshot::channel();
+ self.diff_updated_futures.push(tx);
+
+ let language = self.language.clone();
+ let language_registry = self.language_registry.clone();
+ let unstaged_diff = self.unstaged_diff();
+ let uncommitted_diff = self.uncommitted_diff();
+ let head = self.head_text.clone();
+ let index = self.index_text.clone();
+ let index_changed = self.index_changed;
+ let head_changed = self.head_changed;
+ let language_changed = self.language_changed;
+ let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
+ (Some(index), Some(head)) => Arc::ptr_eq(index, head),
+ (None, None) => true,
+ _ => false,
+ };
+ self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
+ let mut new_unstaged_diff = None;
+ if let Some(unstaged_diff) = &unstaged_diff {
+ new_unstaged_diff = Some(
+ BufferDiff::update_diff(
+ unstaged_diff.clone(),
+ buffer.clone(),
+ index,
+ index_changed,
+ language_changed,
+ language.clone(),
+ language_registry.clone(),
+ &mut cx,
+ )
+ .await?,
+ );
+ }
+
+ let mut new_uncommitted_diff = None;
+ if let Some(uncommitted_diff) = &uncommitted_diff {
+ new_uncommitted_diff = if index_matches_head {
+ new_unstaged_diff.clone()
+ } else {
+ Some(
+ BufferDiff::update_diff(
+ uncommitted_diff.clone(),
+ buffer.clone(),
+ head,
+ head_changed,
+ language_changed,
+ language.clone(),
+ language_registry.clone(),
+ &mut cx,
+ )
+ .await?,
+ )
+ }
+ }
+
+ let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
+ unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
+ {
+ unstaged_diff.update(&mut cx, |diff, cx| {
+ diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
+ })?
+ } else {
+ None
+ };
+
+ if let Some((uncommitted_diff, new_uncommitted_diff)) =
+ uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
+ {
+ uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
+ uncommitted_diff.set_snapshot(
+ &buffer,
+ new_uncommitted_diff,
+ language_changed,
+ unstaged_changed_range,
+ cx,
+ );
+ })?;
+ }
+
+ if let Some(this) = this.upgrade() {
+ this.update(&mut cx, |this, _| {
+ this.index_changed = false;
+ this.head_changed = false;
+ this.language_changed = false;
+ for tx in this.diff_updated_futures.drain(..) {
+ tx.send(()).ok();
+ }
+ })?;
+ }
+
+ Ok(())
+ }));
+
+ rx
+ }
+}
+
fn make_remote_delegate(
this: Entity<GitStore>,
project_id: u64,
@@ -665,6 +665,7 @@ impl Hover {
enum EntitySubscription {
Project(PendingEntitySubscription<Project>),
BufferStore(PendingEntitySubscription<BufferStore>),
+ GitStore(PendingEntitySubscription<GitStore>),
WorktreeStore(PendingEntitySubscription<WorktreeStore>),
LspStore(PendingEntitySubscription<LspStore>),
SettingsObserver(PendingEntitySubscription<SettingsObserver>),
@@ -863,7 +864,6 @@ impl Project {
buffer_store.clone(),
environment.clone(),
fs.clone(),
- client.clone().into(),
cx,
)
});
@@ -992,7 +992,6 @@ impl Project {
buffer_store.clone(),
environment.clone(),
ssh_proto.clone(),
- ProjectId(SSH_PROJECT_ID),
cx,
)
});
@@ -1109,6 +1108,7 @@ impl Project {
let subscriptions = [
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id)?),
EntitySubscription::BufferStore(client.subscribe_to_entity::<BufferStore>(remote_id)?),
+ EntitySubscription::GitStore(client.subscribe_to_entity::<GitStore>(remote_id)?),
EntitySubscription::WorktreeStore(
client.subscribe_to_entity::<WorktreeStore>(remote_id)?,
),
@@ -1137,7 +1137,7 @@ impl Project {
async fn from_join_project_response(
response: TypedEnvelope<proto::JoinProjectResponse>,
- subscriptions: [EntitySubscription; 5],
+ subscriptions: [EntitySubscription; 6],
client: Arc<Client>,
run_tasks: bool,
user_store: Entity<UserStore>,
@@ -1254,7 +1254,7 @@ impl Project {
remote_id,
replica_id,
},
- git_store,
+ git_store: git_store.clone(),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@@ -1284,6 +1284,9 @@ impl Project {
EntitySubscription::WorktreeStore(subscription) => {
subscription.set_entity(&worktree_store, &mut cx)
}
+ EntitySubscription::GitStore(subscription) => {
+ subscription.set_entity(&git_store, &mut cx)
+ }
EntitySubscription::SettingsObserver(subscription) => {
subscription.set_entity(&settings_observer, &mut cx)
}
@@ -1874,6 +1877,9 @@ impl Project {
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.shared(project_id, self.client.clone().into(), cx)
});
+ self.git_store.update(cx, |git_store, cx| {
+ git_store.shared(project_id, self.client.clone().into(), cx)
+ });
self.client_state = ProjectClientState::Shared {
remote_id: project_id,
@@ -1955,6 +1961,9 @@ impl Project {
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.unshared(cx);
});
+ self.git_store.update(cx, |git_store, cx| {
+ git_store.unshared(cx);
+ });
self.client
.send(proto::UnshareProject {
@@ -2180,10 +2189,8 @@ impl Project {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
-
- self.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.open_unstaged_diff(buffer, cx)
- })
+ self.git_store
+ .update(cx, |git_store, cx| git_store.open_unstaged_diff(buffer, cx))
}
pub fn open_uncommitted_diff(
@@ -2194,9 +2201,8 @@ impl Project {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
-
- self.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.open_uncommitted_diff(buffer, cx)
+ self.git_store.update(cx, |git_store, cx| {
+ git_store.open_uncommitted_diff(buffer, cx)
})
}
@@ -2755,8 +2761,8 @@ impl Project {
if buffers.is_empty() {
None
} else {
- Some(this.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.recalculate_buffer_diffs(buffers, cx)
+ Some(this.git_store.update(cx, |git_store, cx| {
+ git_store.recalculate_buffer_diffs(buffers, cx)
}))
}
})
@@ -4008,6 +4014,9 @@ impl Project {
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
}
});
+ this.git_store.update(cx, |git_store, _| {
+ git_store.forget_shared_diffs_for(&peer_id);
+ });
cx.emit(Event::CollaboratorLeft(peer_id));
Ok(())
@@ -6414,8 +6414,6 @@ async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
- let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point());
-
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
.step_by(5)
.map(|i| {
@@ -6444,9 +6442,7 @@ async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
// Stage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
- let hunks = diff
- .hunks_intersecting_range(range.clone(), &snapshot, cx)
- .collect::<Vec<_>>();
+ let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
}
@@ -6480,9 +6476,7 @@ async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
// Unstage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
- let hunks = diff
- .hunks_intersecting_range(range, &snapshot, cx)
- .collect::<Vec<_>>();
+ let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
}
@@ -89,14 +89,15 @@ impl HeadlessProject {
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
let git_store = cx.new(|cx| {
- GitStore::local(
+ let mut store = GitStore::local(
&worktree_store,
buffer_store.clone(),
environment.clone(),
fs.clone(),
- session.clone().into(),
cx,
- )
+ );
+ store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
+ store
});
let prettier_store = cx.new(|cx| {
PrettierStore::new(
@@ -94,6 +94,7 @@ impl BufferId {
self.into()
}
}
+
impl From<BufferId> for u64 {
fn from(id: BufferId) -> Self {
id.0.get()