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