git_tests.rs

  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}