1use std::path::{self, Path, PathBuf};
2
3use call::ActiveCall;
4use client::RECEIVE_TIMEOUT;
5use collections::HashMap;
6use git::{
7 repository::{RepoPath, Worktree as GitWorktree},
8 status::{DiffStat, FileStatus, StatusCode, TrackedStatus},
9};
10use git_ui::{git_panel::GitPanel, project_diff::ProjectDiff};
11use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, VisualTestContext};
12use project::ProjectPath;
13use serde_json::json;
14
15use util::{path, rel_path::rel_path};
16use workspace::{MultiWorkspace, Workspace};
17
18use crate::TestServer;
19
20#[gpui::test]
21async fn test_root_repo_common_dir_sync(
22 executor: BackgroundExecutor,
23 cx_a: &mut TestAppContext,
24 cx_b: &mut TestAppContext,
25) {
26 let mut server = TestServer::start(executor.clone()).await;
27 let client_a = server.create_client(cx_a, "user_a").await;
28 let client_b = server.create_client(cx_b, "user_b").await;
29 server
30 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
31 .await;
32 let active_call_a = cx_a.read(ActiveCall::global);
33
34 // Set up a project whose root IS a git repository.
35 client_a
36 .fs()
37 .insert_tree(
38 path!("/project"),
39 json!({ ".git": {}, "file.txt": "content" }),
40 )
41 .await;
42
43 let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
44 executor.run_until_parked();
45
46 // Host should see root_repo_common_dir pointing to .git at the root.
47 let host_common_dir = project_a.read_with(cx_a, |project, cx| {
48 let worktree = project.worktrees(cx).next().unwrap();
49 worktree.read(cx).snapshot().root_repo_common_dir().cloned()
50 });
51 assert_eq!(
52 host_common_dir.as_deref(),
53 Some(path::Path::new(path!("/project/.git"))),
54 );
55
56 // Share the project and have client B join.
57 let project_id = active_call_a
58 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
59 .await
60 .unwrap();
61 let project_b = client_b.join_remote_project(project_id, cx_b).await;
62 executor.run_until_parked();
63
64 // Guest should see the same root_repo_common_dir as the host.
65 let guest_common_dir = project_b.read_with(cx_b, |project, cx| {
66 let worktree = project.worktrees(cx).next().unwrap();
67 worktree.read(cx).snapshot().root_repo_common_dir().cloned()
68 });
69 assert_eq!(
70 guest_common_dir, host_common_dir,
71 "guest should see the same root_repo_common_dir as host",
72 );
73}
74
75fn collect_diff_stats<C: gpui::AppContext>(
76 panel: &gpui::Entity<GitPanel>,
77 cx: &C,
78) -> HashMap<RepoPath, DiffStat> {
79 panel.read_with(cx, |panel, cx| {
80 let Some(repo) = panel.active_repository() else {
81 return HashMap::default();
82 };
83 let snapshot = repo.read(cx).snapshot();
84 let mut stats = HashMap::default();
85 for entry in snapshot.statuses_by_path.iter() {
86 if let Some(diff_stat) = entry.diff_stat {
87 stats.insert(entry.repo_path.clone(), diff_stat);
88 }
89 }
90 stats
91 })
92}
93
94#[gpui::test]
95async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
96 let mut server = TestServer::start(cx_a.background_executor.clone()).await;
97 let client_a = server.create_client(cx_a, "user_a").await;
98 let client_b = server.create_client(cx_b, "user_b").await;
99 cx_a.set_name("cx_a");
100 cx_b.set_name("cx_b");
101
102 server
103 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
104 .await;
105
106 client_a
107 .fs()
108 .insert_tree(
109 path!("/a"),
110 json!({
111 ".git": {},
112 "changed.txt": "after\n",
113 "unchanged.txt": "unchanged\n",
114 "created.txt": "created\n",
115 "secret.pem": "secret-changed\n",
116 }),
117 )
118 .await;
119
120 client_a.fs().set_head_and_index_for_repo(
121 Path::new(path!("/a/.git")),
122 &[
123 ("changed.txt", "before\n".to_string()),
124 ("unchanged.txt", "unchanged\n".to_string()),
125 ("deleted.txt", "deleted\n".to_string()),
126 ("secret.pem", "shh\n".to_string()),
127 ],
128 );
129 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
130 let active_call_a = cx_a.read(ActiveCall::global);
131 let project_id = active_call_a
132 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
133 .await
134 .unwrap();
135
136 cx_b.update(editor::init);
137 cx_b.update(git_ui::init);
138 let project_b = client_b.join_remote_project(project_id, cx_b).await;
139 let window_b = cx_b.add_window(|window, cx| {
140 let workspace = cx.new(|cx| {
141 Workspace::new(
142 None,
143 project_b.clone(),
144 client_b.app_state.clone(),
145 window,
146 cx,
147 )
148 });
149 MultiWorkspace::new(workspace, window, cx)
150 });
151 let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
152 let workspace_b = window_b
153 .root(cx_b)
154 .unwrap()
155 .read_with(cx_b, |multi_workspace, _| {
156 multi_workspace.workspace().clone()
157 });
158
159 cx_b.update(|window, cx| {
160 window
161 .focused(cx)
162 .unwrap()
163 .dispatch_action(&git_ui::project_diff::Diff, window, cx)
164 });
165 let diff = workspace_b.update(cx_b, |workspace, cx| {
166 workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx)
167 });
168 let diff = diff.unwrap();
169 cx_b.run_until_parked();
170
171 diff.update(cx_b, |diff, cx| {
172 assert_eq!(
173 diff.excerpt_paths(cx),
174 vec![
175 rel_path("changed.txt").into_arc(),
176 rel_path("deleted.txt").into_arc(),
177 rel_path("created.txt").into_arc()
178 ]
179 );
180 });
181
182 client_a
183 .fs()
184 .insert_tree(
185 path!("/a"),
186 json!({
187 ".git": {},
188 "changed.txt": "before\n",
189 "unchanged.txt": "changed\n",
190 "created.txt": "created\n",
191 "secret.pem": "secret-changed\n",
192 }),
193 )
194 .await;
195 cx_b.run_until_parked();
196
197 project_b.update(cx_b, |project, cx| {
198 let project_path = ProjectPath {
199 worktree_id,
200 path: rel_path("unchanged.txt").into(),
201 };
202 let status = project.project_path_git_status(&project_path, cx);
203 assert_eq!(
204 status.unwrap(),
205 FileStatus::Tracked(TrackedStatus {
206 worktree_status: StatusCode::Modified,
207 index_status: StatusCode::Unmodified,
208 })
209 );
210 });
211
212 diff.update(cx_b, |diff, cx| {
213 assert_eq!(
214 diff.excerpt_paths(cx),
215 vec![
216 rel_path("deleted.txt").into_arc(),
217 rel_path("unchanged.txt").into_arc(),
218 rel_path("created.txt").into_arc()
219 ]
220 );
221 });
222}
223
224#[gpui::test]
225async fn test_remote_git_worktrees(
226 executor: BackgroundExecutor,
227 cx_a: &mut TestAppContext,
228 cx_b: &mut TestAppContext,
229) {
230 let mut server = TestServer::start(executor.clone()).await;
231 let client_a = server.create_client(cx_a, "user_a").await;
232 let client_b = server.create_client(cx_b, "user_b").await;
233 server
234 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
235 .await;
236 let active_call_a = cx_a.read(ActiveCall::global);
237
238 client_a
239 .fs()
240 .insert_tree(
241 path!("/project"),
242 json!({ ".git": {}, "file.txt": "content" }),
243 )
244 .await;
245
246 let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
247
248 let project_id = active_call_a
249 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
250 .await
251 .unwrap();
252 let project_b = client_b.join_remote_project(project_id, cx_b).await;
253
254 executor.run_until_parked();
255
256 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
257
258 // Initially only the main worktree (the repo itself) should be present
259 let worktrees = cx_b
260 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
261 .await
262 .unwrap()
263 .unwrap();
264 assert_eq!(worktrees.len(), 1);
265 assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
266
267 // Client B creates a git worktree via the remote project
268 let worktree_directory = PathBuf::from(path!("/project"));
269 cx_b.update(|cx| {
270 repo_b.update(cx, |repository, _| {
271 repository.create_worktree(
272 "feature-branch".to_string(),
273 worktree_directory.join("feature-branch"),
274 Some("abc123".to_string()),
275 )
276 })
277 })
278 .await
279 .unwrap()
280 .unwrap();
281
282 executor.run_until_parked();
283
284 // Client B lists worktrees — should see main + the one just created
285 let worktrees = cx_b
286 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
287 .await
288 .unwrap()
289 .unwrap();
290 assert_eq!(worktrees.len(), 2);
291 assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
292 assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
293 assert_eq!(
294 worktrees[1].ref_name,
295 Some("refs/heads/feature-branch".into())
296 );
297 assert_eq!(worktrees[1].sha.as_ref(), "abc123");
298
299 // Verify from the host side that the worktree was actually created
300 let host_worktrees = {
301 let repo_a = cx_a.update(|cx| {
302 project_a
303 .read(cx)
304 .repositories(cx)
305 .values()
306 .next()
307 .unwrap()
308 .clone()
309 });
310 cx_a.update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
311 .await
312 .unwrap()
313 .unwrap()
314 };
315 assert_eq!(host_worktrees.len(), 2);
316 assert_eq!(host_worktrees[0].path, PathBuf::from(path!("/project")));
317 assert_eq!(
318 host_worktrees[1].path,
319 worktree_directory.join("feature-branch")
320 );
321
322 // Client B creates a second git worktree without an explicit commit
323 cx_b.update(|cx| {
324 repo_b.update(cx, |repository, _| {
325 repository.create_worktree(
326 "bugfix-branch".to_string(),
327 worktree_directory.join("bugfix-branch"),
328 None,
329 )
330 })
331 })
332 .await
333 .unwrap()
334 .unwrap();
335
336 executor.run_until_parked();
337
338 // Client B lists worktrees — should now have main + two created
339 let worktrees = cx_b
340 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
341 .await
342 .unwrap()
343 .unwrap();
344 assert_eq!(worktrees.len(), 3);
345
346 let feature_worktree = worktrees
347 .iter()
348 .find(|worktree| worktree.ref_name == Some("refs/heads/feature-branch".into()))
349 .expect("should find feature-branch worktree");
350 assert_eq!(
351 feature_worktree.path,
352 worktree_directory.join("feature-branch")
353 );
354
355 let bugfix_worktree = worktrees
356 .iter()
357 .find(|worktree| worktree.ref_name == Some("refs/heads/bugfix-branch".into()))
358 .expect("should find bugfix-branch worktree");
359 assert_eq!(
360 bugfix_worktree.path,
361 worktree_directory.join("bugfix-branch")
362 );
363 assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha");
364
365 // Client B (guest) attempts to rename a worktree. This should fail
366 // because worktree renaming is not forwarded through collab
367 let rename_result = cx_b
368 .update(|cx| {
369 repo_b.update(cx, |repository, _| {
370 repository.rename_worktree(
371 worktree_directory.join("feature-branch"),
372 worktree_directory.join("renamed-branch"),
373 )
374 })
375 })
376 .await
377 .unwrap();
378 assert!(
379 rename_result.is_err(),
380 "Guest should not be able to rename worktrees via collab"
381 );
382
383 executor.run_until_parked();
384
385 // Verify worktrees are unchanged — still 3
386 let worktrees = cx_b
387 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
388 .await
389 .unwrap()
390 .unwrap();
391 assert_eq!(
392 worktrees.len(),
393 3,
394 "Worktree count should be unchanged after failed rename"
395 );
396
397 // Client B (guest) attempts to remove a worktree. This should fail
398 // because worktree removal is not forwarded through collab
399 let remove_result = cx_b
400 .update(|cx| {
401 repo_b.update(cx, |repository, _| {
402 repository.remove_worktree(worktree_directory.join("feature-branch"), false)
403 })
404 })
405 .await
406 .unwrap();
407 assert!(
408 remove_result.is_err(),
409 "Guest should not be able to remove worktrees via collab"
410 );
411
412 executor.run_until_parked();
413
414 // Verify worktrees are unchanged — still 3
415 let worktrees = cx_b
416 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
417 .await
418 .unwrap()
419 .unwrap();
420 assert_eq!(
421 worktrees.len(),
422 3,
423 "Worktree count should be unchanged after failed removal"
424 );
425}
426
427#[gpui::test]
428async fn test_linked_worktrees_sync(
429 executor: BackgroundExecutor,
430 cx_a: &mut TestAppContext,
431 cx_b: &mut TestAppContext,
432 cx_c: &mut TestAppContext,
433) {
434 let mut server = TestServer::start(executor.clone()).await;
435 let client_a = server.create_client(cx_a, "user_a").await;
436 let client_b = server.create_client(cx_b, "user_b").await;
437 let client_c = server.create_client(cx_c, "user_c").await;
438 server
439 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
440 .await;
441 let active_call_a = cx_a.read(ActiveCall::global);
442
443 // Set up a git repo with two linked worktrees already present.
444 client_a
445 .fs()
446 .insert_tree(
447 path!("/project"),
448 json!({ ".git": {}, "file.txt": "content" }),
449 )
450 .await;
451
452 let fs = client_a.fs();
453 fs.add_linked_worktree_for_repo(
454 Path::new(path!("/project/.git")),
455 true,
456 GitWorktree {
457 path: PathBuf::from(path!("/worktrees/feature-branch")),
458 ref_name: Some("refs/heads/feature-branch".into()),
459 sha: "bbb222".into(),
460 is_main: false,
461 },
462 )
463 .await;
464 fs.add_linked_worktree_for_repo(
465 Path::new(path!("/project/.git")),
466 true,
467 GitWorktree {
468 path: PathBuf::from(path!("/worktrees/bugfix-branch")),
469 ref_name: Some("refs/heads/bugfix-branch".into()),
470 sha: "ccc333".into(),
471 is_main: false,
472 },
473 )
474 .await;
475
476 let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
477
478 // Wait for git scanning to complete on the host.
479 executor.run_until_parked();
480
481 // Verify the host sees 2 linked worktrees (main worktree is filtered out).
482 let host_linked = project_a.read_with(cx_a, |project, cx| {
483 let repos = project.repositories(cx);
484 assert_eq!(repos.len(), 1, "host should have exactly 1 repository");
485 let repo = repos.values().next().unwrap();
486 repo.read(cx).linked_worktrees().to_vec()
487 });
488 assert_eq!(
489 host_linked.len(),
490 2,
491 "host should have 2 linked worktrees (main filtered out)"
492 );
493 assert_eq!(
494 host_linked[0].path,
495 PathBuf::from(path!("/worktrees/bugfix-branch"))
496 );
497 assert_eq!(
498 host_linked[0].ref_name,
499 Some("refs/heads/bugfix-branch".into())
500 );
501 assert_eq!(host_linked[0].sha.as_ref(), "ccc333");
502 assert_eq!(
503 host_linked[1].path,
504 PathBuf::from(path!("/worktrees/feature-branch"))
505 );
506 assert_eq!(
507 host_linked[1].ref_name,
508 Some("refs/heads/feature-branch".into())
509 );
510 assert_eq!(host_linked[1].sha.as_ref(), "bbb222");
511
512 // Share the project and have client B join.
513 let project_id = active_call_a
514 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
515 .await
516 .unwrap();
517 let project_b = client_b.join_remote_project(project_id, cx_b).await;
518
519 executor.run_until_parked();
520
521 // Verify the guest sees the same linked worktrees as the host.
522 let guest_linked = project_b.read_with(cx_b, |project, cx| {
523 let repos = project.repositories(cx);
524 assert_eq!(repos.len(), 1, "guest should have exactly 1 repository");
525 let repo = repos.values().next().unwrap();
526 repo.read(cx).linked_worktrees().to_vec()
527 });
528 assert_eq!(
529 guest_linked, host_linked,
530 "guest's linked_worktrees should match host's after initial sync"
531 );
532
533 // Now mutate: add a third linked worktree on the host side.
534 client_a
535 .fs()
536 .add_linked_worktree_for_repo(
537 Path::new(path!("/project/.git")),
538 true,
539 GitWorktree {
540 path: PathBuf::from(path!("/worktrees/hotfix-branch")),
541 ref_name: Some("refs/heads/hotfix-branch".into()),
542 sha: "ddd444".into(),
543 is_main: false,
544 },
545 )
546 .await;
547
548 // Wait for the host to re-scan and propagate the update.
549 executor.run_until_parked();
550
551 // Verify host now sees 3 linked worktrees.
552 let host_linked_updated = project_a.read_with(cx_a, |project, cx| {
553 let repos = project.repositories(cx);
554 let repo = repos.values().next().unwrap();
555 repo.read(cx).linked_worktrees().to_vec()
556 });
557 assert_eq!(
558 host_linked_updated.len(),
559 3,
560 "host should now have 3 linked worktrees"
561 );
562 assert_eq!(
563 host_linked_updated[2].path,
564 PathBuf::from(path!("/worktrees/hotfix-branch"))
565 );
566
567 // Verify the guest also received the update.
568 let guest_linked_updated = project_b.read_with(cx_b, |project, cx| {
569 let repos = project.repositories(cx);
570 let repo = repos.values().next().unwrap();
571 repo.read(cx).linked_worktrees().to_vec()
572 });
573 assert_eq!(
574 guest_linked_updated, host_linked_updated,
575 "guest's linked_worktrees should match host's after update"
576 );
577
578 // Now mutate: remove one linked worktree from the host side.
579 client_a
580 .fs()
581 .remove_worktree_for_repo(
582 Path::new(path!("/project/.git")),
583 true,
584 "refs/heads/bugfix-branch",
585 )
586 .await;
587
588 executor.run_until_parked();
589
590 // Verify host now sees 2 linked worktrees (feature-branch and hotfix-branch).
591 let host_linked_after_removal = project_a.read_with(cx_a, |project, cx| {
592 let repos = project.repositories(cx);
593 let repo = repos.values().next().unwrap();
594 repo.read(cx).linked_worktrees().to_vec()
595 });
596 assert_eq!(
597 host_linked_after_removal.len(),
598 2,
599 "host should have 2 linked worktrees after removal"
600 );
601 assert!(
602 host_linked_after_removal
603 .iter()
604 .all(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into())),
605 "bugfix-branch should have been removed"
606 );
607
608 // Verify the guest also reflects the removal.
609 let guest_linked_after_removal = project_b.read_with(cx_b, |project, cx| {
610 let repos = project.repositories(cx);
611 let repo = repos.values().next().unwrap();
612 repo.read(cx).linked_worktrees().to_vec()
613 });
614 assert_eq!(
615 guest_linked_after_removal, host_linked_after_removal,
616 "guest's linked_worktrees should match host's after removal"
617 );
618
619 // Test DB roundtrip: client C joins late, getting state from the database.
620 // This verifies that linked_worktrees are persisted and restored correctly.
621 let project_c = client_c.join_remote_project(project_id, cx_c).await;
622 executor.run_until_parked();
623
624 let late_joiner_linked = project_c.read_with(cx_c, |project, cx| {
625 let repos = project.repositories(cx);
626 assert_eq!(
627 repos.len(),
628 1,
629 "late joiner should have exactly 1 repository"
630 );
631 let repo = repos.values().next().unwrap();
632 repo.read(cx).linked_worktrees().to_vec()
633 });
634 assert_eq!(
635 late_joiner_linked, host_linked_after_removal,
636 "late-joining client's linked_worktrees should match host's (DB roundtrip)"
637 );
638
639 // Test reconnection: disconnect client B (guest) and reconnect.
640 // After rejoining, client B should get linked_worktrees back from the DB.
641 server.disconnect_client(client_b.peer_id().unwrap());
642 executor.advance_clock(RECEIVE_TIMEOUT);
643 executor.run_until_parked();
644
645 // Client B reconnects automatically.
646 executor.advance_clock(RECEIVE_TIMEOUT);
647 executor.run_until_parked();
648
649 // Verify client B still has the correct linked worktrees after reconnection.
650 let guest_linked_after_reconnect = project_b.read_with(cx_b, |project, cx| {
651 let repos = project.repositories(cx);
652 assert_eq!(
653 repos.len(),
654 1,
655 "guest should still have exactly 1 repository after reconnect"
656 );
657 let repo = repos.values().next().unwrap();
658 repo.read(cx).linked_worktrees().to_vec()
659 });
660 assert_eq!(
661 guest_linked_after_reconnect, host_linked_after_removal,
662 "guest's linked_worktrees should survive guest disconnect/reconnect"
663 );
664}
665
666#[gpui::test]
667async fn test_diff_stat_sync_between_host_and_downstream_client(
668 cx_a: &mut TestAppContext,
669 cx_b: &mut TestAppContext,
670 cx_c: &mut TestAppContext,
671) {
672 let mut server = TestServer::start(cx_a.background_executor.clone()).await;
673 let client_a = server.create_client(cx_a, "user_a").await;
674 let client_b = server.create_client(cx_b, "user_b").await;
675 let client_c = server.create_client(cx_c, "user_c").await;
676
677 server
678 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
679 .await;
680
681 let fs = client_a.fs();
682 fs.insert_tree(
683 path!("/code"),
684 json!({
685 "project1": {
686 ".git": {},
687 "src": {
688 "lib.rs": "line1\nline2\nline3\n",
689 "new_file.rs": "added1\nadded2\n",
690 },
691 "README.md": "# project 1",
692 }
693 }),
694 )
695 .await;
696
697 let dot_git = Path::new(path!("/code/project1/.git"));
698 fs.set_head_for_repo(
699 dot_git,
700 &[
701 ("src/lib.rs", "line1\nold_line2\n".into()),
702 ("src/deleted.rs", "was_here\n".into()),
703 ],
704 "deadbeef",
705 );
706 fs.set_index_for_repo(
707 dot_git,
708 &[
709 ("src/lib.rs", "line1\nold_line2\nline3\nline4\n".into()),
710 ("src/staged_only.rs", "x\ny\n".into()),
711 ("src/new_file.rs", "added1\nadded2\n".into()),
712 ("README.md", "# project 1".into()),
713 ],
714 );
715
716 let (project_a, worktree_id) = client_a
717 .build_local_project(path!("/code/project1"), cx_a)
718 .await;
719 let active_call_a = cx_a.read(ActiveCall::global);
720 let project_id = active_call_a
721 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
722 .await
723 .unwrap();
724 let project_b = client_b.join_remote_project(project_id, cx_b).await;
725 let _project_c = client_c.join_remote_project(project_id, cx_c).await;
726 cx_a.run_until_parked();
727
728 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
729 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
730
731 let panel_a = workspace_a.update_in(cx_a, GitPanel::new_test);
732 workspace_a.update_in(cx_a, |workspace, window, cx| {
733 workspace.add_panel(panel_a.clone(), window, cx);
734 });
735
736 let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
737 workspace_b.update_in(cx_b, |workspace, window, cx| {
738 workspace.add_panel(panel_b.clone(), window, cx);
739 });
740
741 cx_a.run_until_parked();
742
743 let stats_a = collect_diff_stats(&panel_a, cx_a);
744 let stats_b = collect_diff_stats(&panel_b, cx_b);
745
746 let mut expected: HashMap<RepoPath, DiffStat> = HashMap::default();
747 expected.insert(
748 RepoPath::new("src/lib.rs").unwrap(),
749 DiffStat {
750 added: 3,
751 deleted: 2,
752 },
753 );
754 expected.insert(
755 RepoPath::new("src/deleted.rs").unwrap(),
756 DiffStat {
757 added: 0,
758 deleted: 1,
759 },
760 );
761 expected.insert(
762 RepoPath::new("src/new_file.rs").unwrap(),
763 DiffStat {
764 added: 2,
765 deleted: 0,
766 },
767 );
768 expected.insert(
769 RepoPath::new("README.md").unwrap(),
770 DiffStat {
771 added: 1,
772 deleted: 0,
773 },
774 );
775 assert_eq!(stats_a, expected, "host diff stats should match expected");
776 assert_eq!(stats_a, stats_b, "host and remote should agree");
777
778 let buffer_a = project_a
779 .update(cx_a, |p, cx| {
780 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
781 })
782 .await
783 .unwrap();
784
785 let _buffer_b = project_b
786 .update(cx_b, |p, cx| {
787 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
788 })
789 .await
790 .unwrap();
791 cx_a.run_until_parked();
792
793 buffer_a.update(cx_a, |buf, cx| {
794 buf.edit([(buf.len()..buf.len(), "line4\n")], None, cx);
795 });
796 project_a
797 .update(cx_a, |project, cx| {
798 project.save_buffer(buffer_a.clone(), cx)
799 })
800 .await
801 .unwrap();
802 cx_a.run_until_parked();
803
804 let stats_a = collect_diff_stats(&panel_a, cx_a);
805 let stats_b = collect_diff_stats(&panel_b, cx_b);
806
807 let mut expected_after_edit = expected.clone();
808 expected_after_edit.insert(
809 RepoPath::new("src/lib.rs").unwrap(),
810 DiffStat {
811 added: 4,
812 deleted: 2,
813 },
814 );
815 assert_eq!(
816 stats_a, expected_after_edit,
817 "host diff stats should reflect the edit"
818 );
819 assert_eq!(
820 stats_b, expected_after_edit,
821 "remote diff stats should reflect the host's edit"
822 );
823
824 let active_call_b = cx_b.read(ActiveCall::global);
825 active_call_b
826 .update(cx_b, |call, cx| call.hang_up(cx))
827 .await
828 .unwrap();
829 cx_a.run_until_parked();
830
831 let user_id_b = client_b.current_user_id(cx_b).to_proto();
832 active_call_a
833 .update(cx_a, |call, cx| call.invite(user_id_b, None, cx))
834 .await
835 .unwrap();
836 cx_b.run_until_parked();
837 let active_call_b = cx_b.read(ActiveCall::global);
838 active_call_b
839 .update(cx_b, |call, cx| call.accept_incoming(cx))
840 .await
841 .unwrap();
842 cx_a.run_until_parked();
843
844 let project_b = client_b.join_remote_project(project_id, cx_b).await;
845 cx_a.run_until_parked();
846
847 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
848 let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
849 workspace_b.update_in(cx_b, |workspace, window, cx| {
850 workspace.add_panel(panel_b.clone(), window, cx);
851 });
852 cx_b.run_until_parked();
853
854 let stats_b = collect_diff_stats(&panel_b, cx_b);
855 assert_eq!(
856 stats_b, expected_after_edit,
857 "remote diff stats should be restored from the database after rejoining the call"
858 );
859}