Fix some syncing issues with git statuses (#25535)
Like the real app, this one infinite loops if you have a diff in an UnsharedFile. Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
88baf171c3
commit
08539b32d0
11 changed files with 283 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2833,6 +2833,7 @@ dependencies = [
|
|||
"futures 0.3.31",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"git_ui",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
"hex",
|
||||
|
|
|
@ -97,6 +97,7 @@ extension.workspace = true
|
|||
file_finder.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
git_ui = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
hyper.workspace = true
|
||||
|
|
|
@ -13,6 +13,7 @@ mod channel_message_tests;
|
|||
mod channel_tests;
|
||||
mod editor_tests;
|
||||
mod following_tests;
|
||||
mod git_tests;
|
||||
mod integration_tests;
|
||||
mod notification_tests;
|
||||
mod random_channel_buffer_tests;
|
||||
|
|
130
crates/collab/src/tests/git_tests.rs
Normal file
130
crates/collab/src/tests/git_tests.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use call::ActiveCall;
|
||||
use git::status::{FileStatus, StatusCode, TrackedStatus};
|
||||
use git_ui::project_diff::ProjectDiff;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use project::ProjectPath;
|
||||
use serde_json::json;
|
||||
use workspace::Workspace;
|
||||
|
||||
//
|
||||
use crate::tests::TestServer;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.background_executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
cx_a.set_name("cx_a");
|
||||
cx_b.set_name("cx_b");
|
||||
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {},
|
||||
"changed.txt": "after\n",
|
||||
"unchanged.txt": "unchanged\n",
|
||||
"created.txt": "created\n",
|
||||
"secret.pem": "secret-changed\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
client_a.fs().set_git_content_for_repo(
|
||||
Path::new("/a/.git"),
|
||||
&[
|
||||
("changed.txt".into(), "before\n".to_string(), None),
|
||||
("unchanged.txt".into(), "unchanged\n".to_string(), None),
|
||||
("deleted.txt".into(), "deleted\n".to_string(), None),
|
||||
("secret.pem".into(), "shh\n".to_string(), None),
|
||||
],
|
||||
);
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_b.update(editor::init);
|
||||
cx_b.update(git_ui::init);
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let workspace_b = cx_b.add_window(|window, cx| {
|
||||
Workspace::new(
|
||||
None,
|
||||
project_b.clone(),
|
||||
client_b.app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||
let workspace_b = workspace_b.root(cx_b).unwrap();
|
||||
|
||||
cx_b.update(|window, cx| {
|
||||
window
|
||||
.focused(cx)
|
||||
.unwrap()
|
||||
.dispatch_action(&git_ui::project_diff::Diff, window, cx)
|
||||
});
|
||||
let diff = workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx)
|
||||
});
|
||||
let diff = diff.unwrap();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
diff.update(cx_b, |diff, cx| {
|
||||
assert_eq!(
|
||||
diff.excerpt_paths(cx),
|
||||
vec!["changed.txt", "deleted.txt", "created.txt"]
|
||||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {},
|
||||
"changed.txt": "before\n",
|
||||
"unchanged.txt": "changed\n",
|
||||
"created.txt": "created\n",
|
||||
"secret.pem": "secret-changed\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
client_a.fs().recalculate_git_status(Path::new("/a/.git"));
|
||||
cx_b.run_until_parked();
|
||||
|
||||
project_b.update(cx_b, |project, cx| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(PathBuf::from("unchanged.txt")),
|
||||
};
|
||||
let status = project.project_path_git_status(&project_path, cx);
|
||||
assert_eq!(
|
||||
status.unwrap(),
|
||||
FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Unmodified,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
diff.update(cx_b, |diff, cx| {
|
||||
assert_eq!(
|
||||
diff.excerpt_paths(cx),
|
||||
vec!["deleted.txt", "unchanged.txt", "created.txt"]
|
||||
);
|
||||
});
|
||||
}
|
|
@ -5,12 +5,20 @@ mod mac_watcher;
|
|||
pub mod fs_watcher;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::HashMap;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::status::StatusCode;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::status::TrackedStatus;
|
||||
use git::GitHostingProviderRegistry;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::{repository::RepoPath, status::FileStatus};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::collections::HashSet;
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::AsFd;
|
||||
#[cfg(unix)]
|
||||
|
@ -1292,6 +1300,105 @@ impl FakeFs {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn set_git_content_for_repo(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
head_state: &[(RepoPath, String, Option<String>)],
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.head_contents.clear();
|
||||
state.head_contents.extend(
|
||||
head_state
|
||||
.iter()
|
||||
.map(|(path, head_content, _)| (path.clone(), head_content.clone())),
|
||||
);
|
||||
state.index_contents.clear();
|
||||
state.index_contents.extend(head_state.iter().map(
|
||||
|(path, head_content, index_content)| {
|
||||
(
|
||||
path.clone(),
|
||||
index_content.as_ref().unwrap_or(head_content).clone(),
|
||||
)
|
||||
},
|
||||
));
|
||||
});
|
||||
self.recalculate_git_status(dot_git);
|
||||
}
|
||||
|
||||
pub fn recalculate_git_status(&self, dot_git: &Path) {
|
||||
let git_files: HashMap<_, _> = self
|
||||
.files()
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
let repo_path =
|
||||
RepoPath::new(path.strip_prefix(dot_git.parent().unwrap()).ok()?.into());
|
||||
let content = self
|
||||
.read_file_sync(path)
|
||||
.ok()
|
||||
.map(|content| String::from_utf8(content).unwrap());
|
||||
Some((repo_path, content?))
|
||||
})
|
||||
.collect();
|
||||
self.with_git_state(dot_git, false, |state| {
|
||||
state.statuses.clear();
|
||||
let mut paths: HashSet<_> = state.head_contents.keys().collect();
|
||||
paths.extend(state.index_contents.keys());
|
||||
paths.extend(git_files.keys());
|
||||
for path in paths {
|
||||
let head = state.head_contents.get(path);
|
||||
let index = state.index_contents.get(path);
|
||||
let fs = git_files.get(path);
|
||||
let status = match (head, index, fs) {
|
||||
(Some(head), Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: if head == index {
|
||||
StatusCode::Unmodified
|
||||
} else {
|
||||
StatusCode::Modified
|
||||
},
|
||||
worktree_status: if fs == index {
|
||||
StatusCode::Unmodified
|
||||
} else {
|
||||
StatusCode::Modified
|
||||
},
|
||||
}),
|
||||
(Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: if head == index {
|
||||
StatusCode::Unmodified
|
||||
} else {
|
||||
StatusCode::Modified
|
||||
},
|
||||
worktree_status: StatusCode::Deleted,
|
||||
}),
|
||||
(Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Added,
|
||||
}),
|
||||
(Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Deleted,
|
||||
}),
|
||||
(None, Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: if fs == index {
|
||||
StatusCode::Unmodified
|
||||
} else {
|
||||
StatusCode::Modified
|
||||
},
|
||||
}),
|
||||
(None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: StatusCode::Deleted,
|
||||
}),
|
||||
(None, None, Some(_)) => FileStatus::Untracked,
|
||||
(None, None, None) => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
state.statuses.insert(path.clone(), status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.blames.clear();
|
||||
|
|
|
@ -49,3 +49,4 @@ windows.workspace = true
|
|||
|
||||
[features]
|
||||
default = []
|
||||
test-support = ["multi_buffer/test-support"]
|
||||
|
|
|
@ -33,7 +33,7 @@ use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
|
|||
|
||||
actions!(git, [Diff]);
|
||||
|
||||
pub(crate) struct ProjectDiff {
|
||||
pub struct ProjectDiff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
project: Entity<Project>,
|
||||
|
@ -438,6 +438,15 @@ impl ProjectDiff {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn excerpt_paths(&self, cx: &App) -> Vec<String> {
|
||||
self.multibuffer
|
||||
.read(cx)
|
||||
.excerpt_paths()
|
||||
.map(|key| key.path().to_string_lossy().to_string())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiff {}
|
||||
|
|
|
@ -165,6 +165,10 @@ impl PathKey {
|
|||
pub fn namespaced(namespace: &'static str, path: Arc<Path>) -> Self {
|
||||
Self { namespace, path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
pub type MultiBufferPoint = Point;
|
||||
|
@ -1453,6 +1457,11 @@ impl MultiBuffer {
|
|||
excerpt.range.context.start,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
|
||||
self.buffers_by_path.keys()
|
||||
}
|
||||
|
||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||
pub fn set_excerpts_for_path(
|
||||
&mut self,
|
||||
|
|
|
@ -88,6 +88,7 @@ pub enum Message {
|
|||
Fetch(GitRepo),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GitEvent {
|
||||
ActiveRepositoryChanged,
|
||||
FileSystemUpdated,
|
||||
|
|
|
@ -377,7 +377,7 @@ impl WorktreeStore {
|
|||
match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(
|
||||
worktree.read(cx).id(),
|
||||
worktree_id,
|
||||
changes.clone(),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -827,17 +827,35 @@ impl Worktree {
|
|||
cx.spawn(|this, mut cx| async move {
|
||||
while (snapshot_updated_rx.recv().await).is_some() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut git_repos_changed = false;
|
||||
let mut entries_changed = false;
|
||||
let this = this.as_remote_mut().unwrap();
|
||||
{
|
||||
let mut lock = this.background_snapshot.lock();
|
||||
this.snapshot = lock.0.clone();
|
||||
if let Some(tx) = &this.update_observer {
|
||||
for update in lock.1.drain(..) {
|
||||
if !update.updated_entries.is_empty()
|
||||
|| !update.removed_entries.is_empty()
|
||||
{
|
||||
entries_changed = true;
|
||||
}
|
||||
if !update.updated_repositories.is_empty()
|
||||
|| !update.removed_repositories.is_empty()
|
||||
{
|
||||
git_repos_changed = true;
|
||||
}
|
||||
if let Some(tx) = &this.update_observer {
|
||||
tx.unbounded_send(update).ok();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if entries_changed {
|
||||
cx.emit(Event::UpdatedEntries(Arc::default()));
|
||||
}
|
||||
if git_repos_changed {
|
||||
cx.emit(Event::UpdatedGitRepositories(Arc::default()));
|
||||
}
|
||||
cx.notify();
|
||||
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
|
||||
if this.observed_snapshot(*scan_id) {
|
||||
|
@ -5451,7 +5469,6 @@ impl BackgroundScanner {
|
|||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"computed git statuses for repo {repository_name} in {:?}",
|
||||
t0.elapsed()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue