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 git::repository::CreateWorktreeTarget::NewBranch {
273 branch_name: "feature-branch".to_string(),
274 base_sha: Some("abc123".to_string()),
275 },
276 worktree_directory.join("feature-branch"),
277 )
278 })
279 })
280 .await
281 .unwrap()
282 .unwrap();
283
284 executor.run_until_parked();
285
286 // Client B lists worktrees — should see main + the one just created
287 let worktrees = cx_b
288 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
289 .await
290 .unwrap()
291 .unwrap();
292 assert_eq!(worktrees.len(), 2);
293 assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
294 assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
295 assert_eq!(
296 worktrees[1].ref_name,
297 Some("refs/heads/feature-branch".into())
298 );
299 assert_eq!(worktrees[1].sha.as_ref(), "abc123");
300
301 // Verify from the host side that the worktree was actually created
302 let host_worktrees = {
303 let repo_a = cx_a.update(|cx| {
304 project_a
305 .read(cx)
306 .repositories(cx)
307 .values()
308 .next()
309 .unwrap()
310 .clone()
311 });
312 cx_a.update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
313 .await
314 .unwrap()
315 .unwrap()
316 };
317 assert_eq!(host_worktrees.len(), 2);
318 assert_eq!(host_worktrees[0].path, PathBuf::from(path!("/project")));
319 assert_eq!(
320 host_worktrees[1].path,
321 worktree_directory.join("feature-branch")
322 );
323
324 // Client B creates a second git worktree without an explicit commit
325 cx_b.update(|cx| {
326 repo_b.update(cx, |repository, _| {
327 repository.create_worktree(
328 git::repository::CreateWorktreeTarget::NewBranch {
329 branch_name: "bugfix-branch".to_string(),
330 base_sha: None,
331 },
332 worktree_directory.join("bugfix-branch"),
333 )
334 })
335 })
336 .await
337 .unwrap()
338 .unwrap();
339
340 executor.run_until_parked();
341
342 // Client B lists worktrees — should now have main + two created
343 let worktrees = cx_b
344 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
345 .await
346 .unwrap()
347 .unwrap();
348 assert_eq!(worktrees.len(), 3);
349
350 let feature_worktree = worktrees
351 .iter()
352 .find(|worktree| worktree.ref_name == Some("refs/heads/feature-branch".into()))
353 .expect("should find feature-branch worktree");
354 assert_eq!(
355 feature_worktree.path,
356 worktree_directory.join("feature-branch")
357 );
358
359 let bugfix_worktree = worktrees
360 .iter()
361 .find(|worktree| worktree.ref_name == Some("refs/heads/bugfix-branch".into()))
362 .expect("should find bugfix-branch worktree");
363 assert_eq!(
364 bugfix_worktree.path,
365 worktree_directory.join("bugfix-branch")
366 );
367 assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha");
368
369 // Client B (guest) attempts to rename a worktree. This should fail
370 // because worktree renaming is not forwarded through collab
371 let rename_result = cx_b
372 .update(|cx| {
373 repo_b.update(cx, |repository, _| {
374 repository.rename_worktree(
375 worktree_directory.join("feature-branch"),
376 worktree_directory.join("renamed-branch"),
377 )
378 })
379 })
380 .await
381 .unwrap();
382 assert!(
383 rename_result.is_err(),
384 "Guest should not be able to rename worktrees via collab"
385 );
386
387 executor.run_until_parked();
388
389 // Verify worktrees are unchanged — still 3
390 let worktrees = cx_b
391 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
392 .await
393 .unwrap()
394 .unwrap();
395 assert_eq!(
396 worktrees.len(),
397 3,
398 "Worktree count should be unchanged after failed rename"
399 );
400
401 // Client B (guest) attempts to remove a worktree. This should fail
402 // because worktree removal is not forwarded through collab
403 let remove_result = cx_b
404 .update(|cx| {
405 repo_b.update(cx, |repository, _| {
406 repository.remove_worktree(worktree_directory.join("feature-branch"), false)
407 })
408 })
409 .await
410 .unwrap();
411 assert!(
412 remove_result.is_err(),
413 "Guest should not be able to remove worktrees via collab"
414 );
415
416 executor.run_until_parked();
417
418 // Verify worktrees are unchanged — still 3
419 let worktrees = cx_b
420 .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
421 .await
422 .unwrap()
423 .unwrap();
424 assert_eq!(
425 worktrees.len(),
426 3,
427 "Worktree count should be unchanged after failed removal"
428 );
429}
430
431#[gpui::test]
432async fn test_remote_git_head_sha(
433 executor: BackgroundExecutor,
434 cx_a: &mut TestAppContext,
435 cx_b: &mut TestAppContext,
436) {
437 let mut server = TestServer::start(executor.clone()).await;
438 let client_a = server.create_client(cx_a, "user_a").await;
439 let client_b = server.create_client(cx_b, "user_b").await;
440 server
441 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
442 .await;
443 let active_call_a = cx_a.read(ActiveCall::global);
444
445 client_a
446 .fs()
447 .insert_tree(
448 path!("/project"),
449 json!({ ".git": {}, "file.txt": "content" }),
450 )
451 .await;
452
453 let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
454 let local_head_sha = cx_a.update(|cx| {
455 project_a
456 .read(cx)
457 .active_repository(cx)
458 .unwrap()
459 .update(cx, |repository, _| repository.head_sha())
460 });
461 let local_head_sha = local_head_sha.await.unwrap().unwrap();
462
463 let project_id = active_call_a
464 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
465 .await
466 .unwrap();
467 let project_b = client_b.join_remote_project(project_id, cx_b).await;
468
469 executor.run_until_parked();
470
471 let remote_head_sha = cx_b.update(|cx| {
472 project_b
473 .read(cx)
474 .active_repository(cx)
475 .unwrap()
476 .update(cx, |repository, _| repository.head_sha())
477 });
478 let remote_head_sha = remote_head_sha.await.unwrap();
479
480 assert_eq!(remote_head_sha.unwrap(), local_head_sha);
481}
482
483#[gpui::test]
484async fn test_linked_worktrees_sync(
485 executor: BackgroundExecutor,
486 cx_a: &mut TestAppContext,
487 cx_b: &mut TestAppContext,
488 cx_c: &mut TestAppContext,
489) {
490 let mut server = TestServer::start(executor.clone()).await;
491 let client_a = server.create_client(cx_a, "user_a").await;
492 let client_b = server.create_client(cx_b, "user_b").await;
493 let client_c = server.create_client(cx_c, "user_c").await;
494 server
495 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
496 .await;
497 let active_call_a = cx_a.read(ActiveCall::global);
498
499 // Set up a git repo with two linked worktrees already present.
500 client_a
501 .fs()
502 .insert_tree(
503 path!("/project"),
504 json!({ ".git": {}, "file.txt": "content" }),
505 )
506 .await;
507
508 let fs = client_a.fs();
509 fs.add_linked_worktree_for_repo(
510 Path::new(path!("/project/.git")),
511 true,
512 GitWorktree {
513 path: PathBuf::from(path!("/worktrees/feature-branch")),
514 ref_name: Some("refs/heads/feature-branch".into()),
515 sha: "bbb222".into(),
516 is_main: false,
517 is_bare: false,
518 },
519 )
520 .await;
521 fs.add_linked_worktree_for_repo(
522 Path::new(path!("/project/.git")),
523 true,
524 GitWorktree {
525 path: PathBuf::from(path!("/worktrees/bugfix-branch")),
526 ref_name: Some("refs/heads/bugfix-branch".into()),
527 sha: "ccc333".into(),
528 is_main: false,
529 is_bare: false,
530 },
531 )
532 .await;
533
534 let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
535
536 // Wait for git scanning to complete on the host.
537 executor.run_until_parked();
538
539 // Verify the host sees 2 linked worktrees (main worktree is filtered out).
540 let host_linked = project_a.read_with(cx_a, |project, cx| {
541 let repos = project.repositories(cx);
542 assert_eq!(repos.len(), 1, "host should have exactly 1 repository");
543 let repo = repos.values().next().unwrap();
544 repo.read(cx).linked_worktrees().to_vec()
545 });
546 assert_eq!(
547 host_linked.len(),
548 2,
549 "host should have 2 linked worktrees (main filtered out)"
550 );
551 assert_eq!(
552 host_linked[0].path,
553 PathBuf::from(path!("/worktrees/bugfix-branch"))
554 );
555 assert_eq!(
556 host_linked[0].ref_name,
557 Some("refs/heads/bugfix-branch".into())
558 );
559 assert_eq!(host_linked[0].sha.as_ref(), "ccc333");
560 assert_eq!(
561 host_linked[1].path,
562 PathBuf::from(path!("/worktrees/feature-branch"))
563 );
564 assert_eq!(
565 host_linked[1].ref_name,
566 Some("refs/heads/feature-branch".into())
567 );
568 assert_eq!(host_linked[1].sha.as_ref(), "bbb222");
569
570 // Share the project and have client B join.
571 let project_id = active_call_a
572 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
573 .await
574 .unwrap();
575 let project_b = client_b.join_remote_project(project_id, cx_b).await;
576
577 executor.run_until_parked();
578
579 // Verify the guest sees the same linked worktrees as the host.
580 let guest_linked = project_b.read_with(cx_b, |project, cx| {
581 let repos = project.repositories(cx);
582 assert_eq!(repos.len(), 1, "guest should have exactly 1 repository");
583 let repo = repos.values().next().unwrap();
584 repo.read(cx).linked_worktrees().to_vec()
585 });
586 assert_eq!(
587 guest_linked, host_linked,
588 "guest's linked_worktrees should match host's after initial sync"
589 );
590
591 // Now mutate: add a third linked worktree on the host side.
592 client_a
593 .fs()
594 .add_linked_worktree_for_repo(
595 Path::new(path!("/project/.git")),
596 true,
597 GitWorktree {
598 path: PathBuf::from(path!("/worktrees/hotfix-branch")),
599 ref_name: Some("refs/heads/hotfix-branch".into()),
600 sha: "ddd444".into(),
601 is_main: false,
602 is_bare: false,
603 },
604 )
605 .await;
606
607 // Wait for the host to re-scan and propagate the update.
608 executor.run_until_parked();
609
610 // Verify host now sees 3 linked worktrees.
611 let host_linked_updated = project_a.read_with(cx_a, |project, cx| {
612 let repos = project.repositories(cx);
613 let repo = repos.values().next().unwrap();
614 repo.read(cx).linked_worktrees().to_vec()
615 });
616 assert_eq!(
617 host_linked_updated.len(),
618 3,
619 "host should now have 3 linked worktrees"
620 );
621 assert_eq!(
622 host_linked_updated[2].path,
623 PathBuf::from(path!("/worktrees/hotfix-branch"))
624 );
625
626 // Verify the guest also received the update.
627 let guest_linked_updated = project_b.read_with(cx_b, |project, cx| {
628 let repos = project.repositories(cx);
629 let repo = repos.values().next().unwrap();
630 repo.read(cx).linked_worktrees().to_vec()
631 });
632 assert_eq!(
633 guest_linked_updated, host_linked_updated,
634 "guest's linked_worktrees should match host's after update"
635 );
636
637 // Now mutate: remove one linked worktree from the host side.
638 client_a
639 .fs()
640 .remove_worktree_for_repo(
641 Path::new(path!("/project/.git")),
642 true,
643 "refs/heads/bugfix-branch",
644 )
645 .await;
646
647 executor.run_until_parked();
648
649 // Verify host now sees 2 linked worktrees (feature-branch and hotfix-branch).
650 let host_linked_after_removal = project_a.read_with(cx_a, |project, cx| {
651 let repos = project.repositories(cx);
652 let repo = repos.values().next().unwrap();
653 repo.read(cx).linked_worktrees().to_vec()
654 });
655 assert_eq!(
656 host_linked_after_removal.len(),
657 2,
658 "host should have 2 linked worktrees after removal"
659 );
660 assert!(
661 host_linked_after_removal
662 .iter()
663 .all(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into())),
664 "bugfix-branch should have been removed"
665 );
666
667 // Verify the guest also reflects the removal.
668 let guest_linked_after_removal = project_b.read_with(cx_b, |project, cx| {
669 let repos = project.repositories(cx);
670 let repo = repos.values().next().unwrap();
671 repo.read(cx).linked_worktrees().to_vec()
672 });
673 assert_eq!(
674 guest_linked_after_removal, host_linked_after_removal,
675 "guest's linked_worktrees should match host's after removal"
676 );
677
678 // Test DB roundtrip: client C joins late, getting state from the database.
679 // This verifies that linked_worktrees are persisted and restored correctly.
680 let project_c = client_c.join_remote_project(project_id, cx_c).await;
681 executor.run_until_parked();
682
683 let late_joiner_linked = project_c.read_with(cx_c, |project, cx| {
684 let repos = project.repositories(cx);
685 assert_eq!(
686 repos.len(),
687 1,
688 "late joiner should have exactly 1 repository"
689 );
690 let repo = repos.values().next().unwrap();
691 repo.read(cx).linked_worktrees().to_vec()
692 });
693 assert_eq!(
694 late_joiner_linked, host_linked_after_removal,
695 "late-joining client's linked_worktrees should match host's (DB roundtrip)"
696 );
697
698 // Test reconnection: disconnect client B (guest) and reconnect.
699 // After rejoining, client B should get linked_worktrees back from the DB.
700 server.disconnect_client(client_b.peer_id().unwrap());
701 executor.advance_clock(RECEIVE_TIMEOUT);
702 executor.run_until_parked();
703
704 // Client B reconnects automatically.
705 executor.advance_clock(RECEIVE_TIMEOUT);
706 executor.run_until_parked();
707
708 // Verify client B still has the correct linked worktrees after reconnection.
709 let guest_linked_after_reconnect = project_b.read_with(cx_b, |project, cx| {
710 let repos = project.repositories(cx);
711 assert_eq!(
712 repos.len(),
713 1,
714 "guest should still have exactly 1 repository after reconnect"
715 );
716 let repo = repos.values().next().unwrap();
717 repo.read(cx).linked_worktrees().to_vec()
718 });
719 assert_eq!(
720 guest_linked_after_reconnect, host_linked_after_removal,
721 "guest's linked_worktrees should survive guest disconnect/reconnect"
722 );
723}
724
725#[gpui::test]
726async fn test_diff_stat_sync_between_host_and_downstream_client(
727 cx_a: &mut TestAppContext,
728 cx_b: &mut TestAppContext,
729 cx_c: &mut TestAppContext,
730) {
731 let mut server = TestServer::start(cx_a.background_executor.clone()).await;
732 let client_a = server.create_client(cx_a, "user_a").await;
733 let client_b = server.create_client(cx_b, "user_b").await;
734 let client_c = server.create_client(cx_c, "user_c").await;
735
736 server
737 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
738 .await;
739
740 let fs = client_a.fs();
741 fs.insert_tree(
742 path!("/code"),
743 json!({
744 "project1": {
745 ".git": {},
746 "src": {
747 "lib.rs": "line1\nline2\nline3\n",
748 "new_file.rs": "added1\nadded2\n",
749 },
750 "README.md": "# project 1",
751 }
752 }),
753 )
754 .await;
755
756 let dot_git = Path::new(path!("/code/project1/.git"));
757 fs.set_head_for_repo(
758 dot_git,
759 &[
760 ("src/lib.rs", "line1\nold_line2\n".into()),
761 ("src/deleted.rs", "was_here\n".into()),
762 ],
763 "deadbeef",
764 );
765 fs.set_index_for_repo(
766 dot_git,
767 &[
768 ("src/lib.rs", "line1\nold_line2\nline3\nline4\n".into()),
769 ("src/staged_only.rs", "x\ny\n".into()),
770 ("src/new_file.rs", "added1\nadded2\n".into()),
771 ("README.md", "# project 1".into()),
772 ],
773 );
774
775 let (project_a, worktree_id) = client_a
776 .build_local_project(path!("/code/project1"), cx_a)
777 .await;
778 let active_call_a = cx_a.read(ActiveCall::global);
779 let project_id = active_call_a
780 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
781 .await
782 .unwrap();
783 let project_b = client_b.join_remote_project(project_id, cx_b).await;
784 let _project_c = client_c.join_remote_project(project_id, cx_c).await;
785 cx_a.run_until_parked();
786
787 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
788 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
789
790 let panel_a = workspace_a.update_in(cx_a, GitPanel::new_test);
791 workspace_a.update_in(cx_a, |workspace, window, cx| {
792 workspace.add_panel(panel_a.clone(), window, cx);
793 });
794
795 let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
796 workspace_b.update_in(cx_b, |workspace, window, cx| {
797 workspace.add_panel(panel_b.clone(), window, cx);
798 });
799
800 cx_a.run_until_parked();
801
802 let stats_a = collect_diff_stats(&panel_a, cx_a);
803 let stats_b = collect_diff_stats(&panel_b, cx_b);
804
805 let mut expected: HashMap<RepoPath, DiffStat> = HashMap::default();
806 expected.insert(
807 RepoPath::new("src/lib.rs").unwrap(),
808 DiffStat {
809 added: 3,
810 deleted: 2,
811 },
812 );
813 expected.insert(
814 RepoPath::new("src/deleted.rs").unwrap(),
815 DiffStat {
816 added: 0,
817 deleted: 1,
818 },
819 );
820 expected.insert(
821 RepoPath::new("src/new_file.rs").unwrap(),
822 DiffStat {
823 added: 2,
824 deleted: 0,
825 },
826 );
827 expected.insert(
828 RepoPath::new("README.md").unwrap(),
829 DiffStat {
830 added: 1,
831 deleted: 0,
832 },
833 );
834 assert_eq!(stats_a, expected, "host diff stats should match expected");
835 assert_eq!(stats_a, stats_b, "host and remote should agree");
836
837 let buffer_a = project_a
838 .update(cx_a, |p, cx| {
839 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
840 })
841 .await
842 .unwrap();
843
844 let _buffer_b = project_b
845 .update(cx_b, |p, cx| {
846 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
847 })
848 .await
849 .unwrap();
850 cx_a.run_until_parked();
851
852 buffer_a.update(cx_a, |buf, cx| {
853 buf.edit([(buf.len()..buf.len(), "line4\n")], None, cx);
854 });
855 project_a
856 .update(cx_a, |project, cx| {
857 project.save_buffer(buffer_a.clone(), cx)
858 })
859 .await
860 .unwrap();
861 cx_a.run_until_parked();
862
863 let stats_a = collect_diff_stats(&panel_a, cx_a);
864 let stats_b = collect_diff_stats(&panel_b, cx_b);
865
866 let mut expected_after_edit = expected.clone();
867 expected_after_edit.insert(
868 RepoPath::new("src/lib.rs").unwrap(),
869 DiffStat {
870 added: 4,
871 deleted: 2,
872 },
873 );
874 assert_eq!(
875 stats_a, expected_after_edit,
876 "host diff stats should reflect the edit"
877 );
878 assert_eq!(
879 stats_b, expected_after_edit,
880 "remote diff stats should reflect the host's edit"
881 );
882
883 let active_call_b = cx_b.read(ActiveCall::global);
884 active_call_b
885 .update(cx_b, |call, cx| call.hang_up(cx))
886 .await
887 .unwrap();
888 cx_a.run_until_parked();
889
890 let user_id_b = client_b.current_user_id(cx_b).to_proto();
891 active_call_a
892 .update(cx_a, |call, cx| call.invite(user_id_b, None, cx))
893 .await
894 .unwrap();
895 cx_b.run_until_parked();
896 let active_call_b = cx_b.read(ActiveCall::global);
897 active_call_b
898 .update(cx_b, |call, cx| call.accept_incoming(cx))
899 .await
900 .unwrap();
901 cx_a.run_until_parked();
902
903 let project_b = client_b.join_remote_project(project_id, cx_b).await;
904 cx_a.run_until_parked();
905
906 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
907 let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
908 workspace_b.update_in(cx_b, |workspace, window, cx| {
909 workspace.add_panel(panel_b.clone(), window, cx);
910 });
911 cx_b.run_until_parked();
912
913 let stats_b = collect_diff_stats(&panel_b, cx_b);
914 assert_eq!(
915 stats_b, expected_after_edit,
916 "remote diff stats should be restored from the database after rejoining the call"
917 );
918}