Skip to content

Commit 08539b3

Browse files
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>
1 parent 88baf17 commit 08539b3

File tree

11 files changed

+283
-6
lines changed

11 files changed

+283
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/collab/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ extension.workspace = true
9797
file_finder.workspace = true
9898
fs = { workspace = true, features = ["test-support"] }
9999
git = { workspace = true, features = ["test-support"] }
100+
git_ui = { workspace = true, features = ["test-support"] }
100101
git_hosting_providers.workspace = true
101102
gpui = { workspace = true, features = ["test-support"] }
102103
hyper.workspace = true

crates/collab/src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod channel_message_tests;
1313
mod channel_tests;
1414
mod editor_tests;
1515
mod following_tests;
16+
mod git_tests;
1617
mod integration_tests;
1718
mod notification_tests;
1819
mod random_channel_buffer_tests;

crates/collab/src/tests/git_tests.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::Arc,
4+
};
5+
6+
use call::ActiveCall;
7+
use git::status::{FileStatus, StatusCode, TrackedStatus};
8+
use git_ui::project_diff::ProjectDiff;
9+
use gpui::{TestAppContext, VisualTestContext};
10+
use project::ProjectPath;
11+
use serde_json::json;
12+
use workspace::Workspace;
13+
14+
//
15+
use crate::tests::TestServer;
16+
17+
#[gpui::test]
18+
async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
19+
let mut server = TestServer::start(cx_a.background_executor.clone()).await;
20+
let client_a = server.create_client(cx_a, "user_a").await;
21+
let client_b = server.create_client(cx_b, "user_b").await;
22+
cx_a.set_name("cx_a");
23+
cx_b.set_name("cx_b");
24+
25+
server
26+
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
27+
.await;
28+
29+
client_a
30+
.fs()
31+
.insert_tree(
32+
"/a",
33+
json!({
34+
".git": {},
35+
"changed.txt": "after\n",
36+
"unchanged.txt": "unchanged\n",
37+
"created.txt": "created\n",
38+
"secret.pem": "secret-changed\n",
39+
}),
40+
)
41+
.await;
42+
43+
client_a.fs().set_git_content_for_repo(
44+
Path::new("/a/.git"),
45+
&[
46+
("changed.txt".into(), "before\n".to_string(), None),
47+
("unchanged.txt".into(), "unchanged\n".to_string(), None),
48+
("deleted.txt".into(), "deleted\n".to_string(), None),
49+
("secret.pem".into(), "shh\n".to_string(), None),
50+
],
51+
);
52+
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
53+
let active_call_a = cx_a.read(ActiveCall::global);
54+
let project_id = active_call_a
55+
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
56+
.await
57+
.unwrap();
58+
59+
cx_b.update(editor::init);
60+
cx_b.update(git_ui::init);
61+
let project_b = client_b.join_remote_project(project_id, cx_b).await;
62+
let workspace_b = cx_b.add_window(|window, cx| {
63+
Workspace::new(
64+
None,
65+
project_b.clone(),
66+
client_b.app_state.clone(),
67+
window,
68+
cx,
69+
)
70+
});
71+
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
72+
let workspace_b = workspace_b.root(cx_b).unwrap();
73+
74+
cx_b.update(|window, cx| {
75+
window
76+
.focused(cx)
77+
.unwrap()
78+
.dispatch_action(&git_ui::project_diff::Diff, window, cx)
79+
});
80+
let diff = workspace_b.update(cx_b, |workspace, cx| {
81+
workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx)
82+
});
83+
let diff = diff.unwrap();
84+
cx_b.run_until_parked();
85+
86+
diff.update(cx_b, |diff, cx| {
87+
assert_eq!(
88+
diff.excerpt_paths(cx),
89+
vec!["changed.txt", "deleted.txt", "created.txt"]
90+
);
91+
});
92+
93+
client_a
94+
.fs()
95+
.insert_tree(
96+
"/a",
97+
json!({
98+
".git": {},
99+
"changed.txt": "before\n",
100+
"unchanged.txt": "changed\n",
101+
"created.txt": "created\n",
102+
"secret.pem": "secret-changed\n",
103+
}),
104+
)
105+
.await;
106+
client_a.fs().recalculate_git_status(Path::new("/a/.git"));
107+
cx_b.run_until_parked();
108+
109+
project_b.update(cx_b, |project, cx| {
110+
let project_path = ProjectPath {
111+
worktree_id,
112+
path: Arc::from(PathBuf::from("unchanged.txt")),
113+
};
114+
let status = project.project_path_git_status(&project_path, cx);
115+
assert_eq!(
116+
status.unwrap(),
117+
FileStatus::Tracked(TrackedStatus {
118+
worktree_status: StatusCode::Modified,
119+
index_status: StatusCode::Unmodified,
120+
})
121+
);
122+
});
123+
124+
diff.update(cx_b, |diff, cx| {
125+
assert_eq!(
126+
diff.excerpt_paths(cx),
127+
vec!["deleted.txt", "unchanged.txt", "created.txt"]
128+
);
129+
});
130+
}

crates/fs/src/fs.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ mod mac_watcher;
55
pub mod fs_watcher;
66

77
use anyhow::{anyhow, Context as _, Result};
8+
#[cfg(any(test, feature = "test-support"))]
9+
use collections::HashMap;
10+
#[cfg(any(test, feature = "test-support"))]
11+
use git::status::StatusCode;
12+
#[cfg(any(test, feature = "test-support"))]
13+
use git::status::TrackedStatus;
814
use git::GitHostingProviderRegistry;
915
#[cfg(any(test, feature = "test-support"))]
1016
use git::{repository::RepoPath, status::FileStatus};
1117

1218
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
1319
use ashpd::desktop::trash;
20+
#[cfg(any(test, feature = "test-support"))]
21+
use std::collections::HashSet;
1422
#[cfg(unix)]
1523
use std::os::fd::AsFd;
1624
#[cfg(unix)]
@@ -1292,6 +1300,105 @@ impl FakeFs {
12921300
});
12931301
}
12941302

1303+
pub fn set_git_content_for_repo(
1304+
&self,
1305+
dot_git: &Path,
1306+
head_state: &[(RepoPath, String, Option<String>)],
1307+
) {
1308+
self.with_git_state(dot_git, true, |state| {
1309+
state.head_contents.clear();
1310+
state.head_contents.extend(
1311+
head_state
1312+
.iter()
1313+
.map(|(path, head_content, _)| (path.clone(), head_content.clone())),
1314+
);
1315+
state.index_contents.clear();
1316+
state.index_contents.extend(head_state.iter().map(
1317+
|(path, head_content, index_content)| {
1318+
(
1319+
path.clone(),
1320+
index_content.as_ref().unwrap_or(head_content).clone(),
1321+
)
1322+
},
1323+
));
1324+
});
1325+
self.recalculate_git_status(dot_git);
1326+
}
1327+
1328+
pub fn recalculate_git_status(&self, dot_git: &Path) {
1329+
let git_files: HashMap<_, _> = self
1330+
.files()
1331+
.iter()
1332+
.filter_map(|path| {
1333+
let repo_path =
1334+
RepoPath::new(path.strip_prefix(dot_git.parent().unwrap()).ok()?.into());
1335+
let content = self
1336+
.read_file_sync(path)
1337+
.ok()
1338+
.map(|content| String::from_utf8(content).unwrap());
1339+
Some((repo_path, content?))
1340+
})
1341+
.collect();
1342+
self.with_git_state(dot_git, false, |state| {
1343+
state.statuses.clear();
1344+
let mut paths: HashSet<_> = state.head_contents.keys().collect();
1345+
paths.extend(state.index_contents.keys());
1346+
paths.extend(git_files.keys());
1347+
for path in paths {
1348+
let head = state.head_contents.get(path);
1349+
let index = state.index_contents.get(path);
1350+
let fs = git_files.get(path);
1351+
let status = match (head, index, fs) {
1352+
(Some(head), Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
1353+
index_status: if head == index {
1354+
StatusCode::Unmodified
1355+
} else {
1356+
StatusCode::Modified
1357+
},
1358+
worktree_status: if fs == index {
1359+
StatusCode::Unmodified
1360+
} else {
1361+
StatusCode::Modified
1362+
},
1363+
}),
1364+
(Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
1365+
index_status: if head == index {
1366+
StatusCode::Unmodified
1367+
} else {
1368+
StatusCode::Modified
1369+
},
1370+
worktree_status: StatusCode::Deleted,
1371+
}),
1372+
(Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
1373+
index_status: StatusCode::Deleted,
1374+
worktree_status: StatusCode::Added,
1375+
}),
1376+
(Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
1377+
index_status: StatusCode::Deleted,
1378+
worktree_status: StatusCode::Deleted,
1379+
}),
1380+
(None, Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
1381+
index_status: StatusCode::Added,
1382+
worktree_status: if fs == index {
1383+
StatusCode::Unmodified
1384+
} else {
1385+
StatusCode::Modified
1386+
},
1387+
}),
1388+
(None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
1389+
index_status: StatusCode::Added,
1390+
worktree_status: StatusCode::Deleted,
1391+
}),
1392+
(None, None, Some(_)) => FileStatus::Untracked,
1393+
(None, None, None) => {
1394+
unreachable!();
1395+
}
1396+
};
1397+
state.statuses.insert(path.clone(), status);
1398+
}
1399+
});
1400+
}
1401+
12951402
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
12961403
self.with_git_state(dot_git, true, |state| {
12971404
state.blames.clear();

crates/git_ui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ windows.workspace = true
4949

5050
[features]
5151
default = []
52+
test-support = ["multi_buffer/test-support"]

crates/git_ui/src/project_diff.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
3333

3434
actions!(git, [Diff]);
3535

36-
pub(crate) struct ProjectDiff {
36+
pub struct ProjectDiff {
3737
multibuffer: Entity<MultiBuffer>,
3838
editor: Entity<Editor>,
3939
project: Entity<Project>,
@@ -438,6 +438,15 @@ impl ProjectDiff {
438438

439439
Ok(())
440440
}
441+
442+
#[cfg(any(test, feature = "test-support"))]
443+
pub fn excerpt_paths(&self, cx: &App) -> Vec<String> {
444+
self.multibuffer
445+
.read(cx)
446+
.excerpt_paths()
447+
.map(|key| key.path().to_string_lossy().to_string())
448+
.collect()
449+
}
441450
}
442451

443452
impl EventEmitter<EditorEvent> for ProjectDiff {}

crates/multi_buffer/src/multi_buffer.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ impl PathKey {
165165
pub fn namespaced(namespace: &'static str, path: Arc<Path>) -> Self {
166166
Self { namespace, path }
167167
}
168+
169+
pub fn path(&self) -> &Arc<Path> {
170+
&self.path
171+
}
168172
}
169173

170174
pub type MultiBufferPoint = Point;
@@ -1453,6 +1457,11 @@ impl MultiBuffer {
14531457
excerpt.range.context.start,
14541458
))
14551459
}
1460+
1461+
pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
1462+
self.buffers_by_path.keys()
1463+
}
1464+
14561465
/// Sets excerpts, returns `true` if at least one new excerpt was added.
14571466
pub fn set_excerpts_for_path(
14581467
&mut self,

crates/project/src/git.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub enum Message {
8888
Fetch(GitRepo),
8989
}
9090

91+
#[derive(Debug)]
9192
pub enum GitEvent {
9293
ActiveRepositoryChanged,
9394
FileSystemUpdated,

crates/project/src/worktree_store.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ impl WorktreeStore {
377377
match event {
378378
worktree::Event::UpdatedEntries(changes) => {
379379
cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(
380-
worktree.read(cx).id(),
380+
worktree_id,
381381
changes.clone(),
382382
));
383383
}

crates/worktree/src/worktree.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -827,17 +827,35 @@ impl Worktree {
827827
cx.spawn(|this, mut cx| async move {
828828
while (snapshot_updated_rx.recv().await).is_some() {
829829
this.update(&mut cx, |this, cx| {
830+
let mut git_repos_changed = false;
831+
let mut entries_changed = false;
830832
let this = this.as_remote_mut().unwrap();
831833
{
832834
let mut lock = this.background_snapshot.lock();
833835
this.snapshot = lock.0.clone();
834-
if let Some(tx) = &this.update_observer {
835-
for update in lock.1.drain(..) {
836+
for update in lock.1.drain(..) {
837+
if !update.updated_entries.is_empty()
838+
|| !update.removed_entries.is_empty()
839+
{
840+
entries_changed = true;
841+
}
842+
if !update.updated_repositories.is_empty()
843+
|| !update.removed_repositories.is_empty()
844+
{
845+
git_repos_changed = true;
846+
}
847+
if let Some(tx) = &this.update_observer {
836848
tx.unbounded_send(update).ok();
837849
}
838850
}
839851
};
840-
cx.emit(Event::UpdatedEntries(Arc::default()));
852+
853+
if entries_changed {
854+
cx.emit(Event::UpdatedEntries(Arc::default()));
855+
}
856+
if git_repos_changed {
857+
cx.emit(Event::UpdatedGitRepositories(Arc::default()));
858+
}
841859
cx.notify();
842860
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
843861
if this.observed_snapshot(*scan_id) {
@@ -5451,7 +5469,6 @@ impl BackgroundScanner {
54515469
else {
54525470
return;
54535471
};
5454-
54555472
log::trace!(
54565473
"computed git statuses for repo {repository_name} in {:?}",
54575474
t0.elapsed()

0 commit comments

Comments
 (0)