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                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        },
518    )
519    .await;
520    fs.add_linked_worktree_for_repo(
521        Path::new(path!("/project/.git")),
522        true,
523        GitWorktree {
524            path: PathBuf::from(path!("/worktrees/bugfix-branch")),
525            ref_name: Some("refs/heads/bugfix-branch".into()),
526            sha: "ccc333".into(),
527            is_main: false,
528        },
529    )
530    .await;
531
532    let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
533
534    // Wait for git scanning to complete on the host.
535    executor.run_until_parked();
536
537    // Verify the host sees 2 linked worktrees (main worktree is filtered out).
538    let host_linked = project_a.read_with(cx_a, |project, cx| {
539        let repos = project.repositories(cx);
540        assert_eq!(repos.len(), 1, "host should have exactly 1 repository");
541        let repo = repos.values().next().unwrap();
542        repo.read(cx).linked_worktrees().to_vec()
543    });
544    assert_eq!(
545        host_linked.len(),
546        2,
547        "host should have 2 linked worktrees (main filtered out)"
548    );
549    assert_eq!(
550        host_linked[0].path,
551        PathBuf::from(path!("/worktrees/bugfix-branch"))
552    );
553    assert_eq!(
554        host_linked[0].ref_name,
555        Some("refs/heads/bugfix-branch".into())
556    );
557    assert_eq!(host_linked[0].sha.as_ref(), "ccc333");
558    assert_eq!(
559        host_linked[1].path,
560        PathBuf::from(path!("/worktrees/feature-branch"))
561    );
562    assert_eq!(
563        host_linked[1].ref_name,
564        Some("refs/heads/feature-branch".into())
565    );
566    assert_eq!(host_linked[1].sha.as_ref(), "bbb222");
567
568    // Share the project and have client B join.
569    let project_id = active_call_a
570        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
571        .await
572        .unwrap();
573    let project_b = client_b.join_remote_project(project_id, cx_b).await;
574
575    executor.run_until_parked();
576
577    // Verify the guest sees the same linked worktrees as the host.
578    let guest_linked = project_b.read_with(cx_b, |project, cx| {
579        let repos = project.repositories(cx);
580        assert_eq!(repos.len(), 1, "guest should have exactly 1 repository");
581        let repo = repos.values().next().unwrap();
582        repo.read(cx).linked_worktrees().to_vec()
583    });
584    assert_eq!(
585        guest_linked, host_linked,
586        "guest's linked_worktrees should match host's after initial sync"
587    );
588
589    // Now mutate: add a third linked worktree on the host side.
590    client_a
591        .fs()
592        .add_linked_worktree_for_repo(
593            Path::new(path!("/project/.git")),
594            true,
595            GitWorktree {
596                path: PathBuf::from(path!("/worktrees/hotfix-branch")),
597                ref_name: Some("refs/heads/hotfix-branch".into()),
598                sha: "ddd444".into(),
599                is_main: false,
600            },
601        )
602        .await;
603
604    // Wait for the host to re-scan and propagate the update.
605    executor.run_until_parked();
606
607    // Verify host now sees 3 linked worktrees.
608    let host_linked_updated = project_a.read_with(cx_a, |project, cx| {
609        let repos = project.repositories(cx);
610        let repo = repos.values().next().unwrap();
611        repo.read(cx).linked_worktrees().to_vec()
612    });
613    assert_eq!(
614        host_linked_updated.len(),
615        3,
616        "host should now have 3 linked worktrees"
617    );
618    assert_eq!(
619        host_linked_updated[2].path,
620        PathBuf::from(path!("/worktrees/hotfix-branch"))
621    );
622
623    // Verify the guest also received the update.
624    let guest_linked_updated = project_b.read_with(cx_b, |project, cx| {
625        let repos = project.repositories(cx);
626        let repo = repos.values().next().unwrap();
627        repo.read(cx).linked_worktrees().to_vec()
628    });
629    assert_eq!(
630        guest_linked_updated, host_linked_updated,
631        "guest's linked_worktrees should match host's after update"
632    );
633
634    // Now mutate: remove one linked worktree from the host side.
635    client_a
636        .fs()
637        .remove_worktree_for_repo(
638            Path::new(path!("/project/.git")),
639            true,
640            "refs/heads/bugfix-branch",
641        )
642        .await;
643
644    executor.run_until_parked();
645
646    // Verify host now sees 2 linked worktrees (feature-branch and hotfix-branch).
647    let host_linked_after_removal = project_a.read_with(cx_a, |project, cx| {
648        let repos = project.repositories(cx);
649        let repo = repos.values().next().unwrap();
650        repo.read(cx).linked_worktrees().to_vec()
651    });
652    assert_eq!(
653        host_linked_after_removal.len(),
654        2,
655        "host should have 2 linked worktrees after removal"
656    );
657    assert!(
658        host_linked_after_removal
659            .iter()
660            .all(|wt| wt.ref_name != Some("refs/heads/bugfix-branch".into())),
661        "bugfix-branch should have been removed"
662    );
663
664    // Verify the guest also reflects the removal.
665    let guest_linked_after_removal = project_b.read_with(cx_b, |project, cx| {
666        let repos = project.repositories(cx);
667        let repo = repos.values().next().unwrap();
668        repo.read(cx).linked_worktrees().to_vec()
669    });
670    assert_eq!(
671        guest_linked_after_removal, host_linked_after_removal,
672        "guest's linked_worktrees should match host's after removal"
673    );
674
675    // Test DB roundtrip: client C joins late, getting state from the database.
676    // This verifies that linked_worktrees are persisted and restored correctly.
677    let project_c = client_c.join_remote_project(project_id, cx_c).await;
678    executor.run_until_parked();
679
680    let late_joiner_linked = project_c.read_with(cx_c, |project, cx| {
681        let repos = project.repositories(cx);
682        assert_eq!(
683            repos.len(),
684            1,
685            "late joiner should have exactly 1 repository"
686        );
687        let repo = repos.values().next().unwrap();
688        repo.read(cx).linked_worktrees().to_vec()
689    });
690    assert_eq!(
691        late_joiner_linked, host_linked_after_removal,
692        "late-joining client's linked_worktrees should match host's (DB roundtrip)"
693    );
694
695    // Test reconnection: disconnect client B (guest) and reconnect.
696    // After rejoining, client B should get linked_worktrees back from the DB.
697    server.disconnect_client(client_b.peer_id().unwrap());
698    executor.advance_clock(RECEIVE_TIMEOUT);
699    executor.run_until_parked();
700
701    // Client B reconnects automatically.
702    executor.advance_clock(RECEIVE_TIMEOUT);
703    executor.run_until_parked();
704
705    // Verify client B still has the correct linked worktrees after reconnection.
706    let guest_linked_after_reconnect = project_b.read_with(cx_b, |project, cx| {
707        let repos = project.repositories(cx);
708        assert_eq!(
709            repos.len(),
710            1,
711            "guest should still have exactly 1 repository after reconnect"
712        );
713        let repo = repos.values().next().unwrap();
714        repo.read(cx).linked_worktrees().to_vec()
715    });
716    assert_eq!(
717        guest_linked_after_reconnect, host_linked_after_removal,
718        "guest's linked_worktrees should survive guest disconnect/reconnect"
719    );
720}
721
722#[gpui::test]
723async fn test_diff_stat_sync_between_host_and_downstream_client(
724    cx_a: &mut TestAppContext,
725    cx_b: &mut TestAppContext,
726    cx_c: &mut TestAppContext,
727) {
728    let mut server = TestServer::start(cx_a.background_executor.clone()).await;
729    let client_a = server.create_client(cx_a, "user_a").await;
730    let client_b = server.create_client(cx_b, "user_b").await;
731    let client_c = server.create_client(cx_c, "user_c").await;
732
733    server
734        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
735        .await;
736
737    let fs = client_a.fs();
738    fs.insert_tree(
739        path!("/code"),
740        json!({
741            "project1": {
742                ".git": {},
743                "src": {
744                    "lib.rs": "line1\nline2\nline3\n",
745                    "new_file.rs": "added1\nadded2\n",
746                },
747                "README.md": "# project 1",
748            }
749        }),
750    )
751    .await;
752
753    let dot_git = Path::new(path!("/code/project1/.git"));
754    fs.set_head_for_repo(
755        dot_git,
756        &[
757            ("src/lib.rs", "line1\nold_line2\n".into()),
758            ("src/deleted.rs", "was_here\n".into()),
759        ],
760        "deadbeef",
761    );
762    fs.set_index_for_repo(
763        dot_git,
764        &[
765            ("src/lib.rs", "line1\nold_line2\nline3\nline4\n".into()),
766            ("src/staged_only.rs", "x\ny\n".into()),
767            ("src/new_file.rs", "added1\nadded2\n".into()),
768            ("README.md", "# project 1".into()),
769        ],
770    );
771
772    let (project_a, worktree_id) = client_a
773        .build_local_project(path!("/code/project1"), cx_a)
774        .await;
775    let active_call_a = cx_a.read(ActiveCall::global);
776    let project_id = active_call_a
777        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
778        .await
779        .unwrap();
780    let project_b = client_b.join_remote_project(project_id, cx_b).await;
781    let _project_c = client_c.join_remote_project(project_id, cx_c).await;
782    cx_a.run_until_parked();
783
784    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
785    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
786
787    let panel_a = workspace_a.update_in(cx_a, GitPanel::new_test);
788    workspace_a.update_in(cx_a, |workspace, window, cx| {
789        workspace.add_panel(panel_a.clone(), window, cx);
790    });
791
792    let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
793    workspace_b.update_in(cx_b, |workspace, window, cx| {
794        workspace.add_panel(panel_b.clone(), window, cx);
795    });
796
797    cx_a.run_until_parked();
798
799    let stats_a = collect_diff_stats(&panel_a, cx_a);
800    let stats_b = collect_diff_stats(&panel_b, cx_b);
801
802    let mut expected: HashMap<RepoPath, DiffStat> = HashMap::default();
803    expected.insert(
804        RepoPath::new("src/lib.rs").unwrap(),
805        DiffStat {
806            added: 3,
807            deleted: 2,
808        },
809    );
810    expected.insert(
811        RepoPath::new("src/deleted.rs").unwrap(),
812        DiffStat {
813            added: 0,
814            deleted: 1,
815        },
816    );
817    expected.insert(
818        RepoPath::new("src/new_file.rs").unwrap(),
819        DiffStat {
820            added: 2,
821            deleted: 0,
822        },
823    );
824    expected.insert(
825        RepoPath::new("README.md").unwrap(),
826        DiffStat {
827            added: 1,
828            deleted: 0,
829        },
830    );
831    assert_eq!(stats_a, expected, "host diff stats should match expected");
832    assert_eq!(stats_a, stats_b, "host and remote should agree");
833
834    let buffer_a = project_a
835        .update(cx_a, |p, cx| {
836            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
837        })
838        .await
839        .unwrap();
840
841    let _buffer_b = project_b
842        .update(cx_b, |p, cx| {
843            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
844        })
845        .await
846        .unwrap();
847    cx_a.run_until_parked();
848
849    buffer_a.update(cx_a, |buf, cx| {
850        buf.edit([(buf.len()..buf.len(), "line4\n")], None, cx);
851    });
852    project_a
853        .update(cx_a, |project, cx| {
854            project.save_buffer(buffer_a.clone(), cx)
855        })
856        .await
857        .unwrap();
858    cx_a.run_until_parked();
859
860    let stats_a = collect_diff_stats(&panel_a, cx_a);
861    let stats_b = collect_diff_stats(&panel_b, cx_b);
862
863    let mut expected_after_edit = expected.clone();
864    expected_after_edit.insert(
865        RepoPath::new("src/lib.rs").unwrap(),
866        DiffStat {
867            added: 4,
868            deleted: 2,
869        },
870    );
871    assert_eq!(
872        stats_a, expected_after_edit,
873        "host diff stats should reflect the edit"
874    );
875    assert_eq!(
876        stats_b, expected_after_edit,
877        "remote diff stats should reflect the host's edit"
878    );
879
880    let active_call_b = cx_b.read(ActiveCall::global);
881    active_call_b
882        .update(cx_b, |call, cx| call.hang_up(cx))
883        .await
884        .unwrap();
885    cx_a.run_until_parked();
886
887    let user_id_b = client_b.current_user_id(cx_b).to_proto();
888    active_call_a
889        .update(cx_a, |call, cx| call.invite(user_id_b, None, cx))
890        .await
891        .unwrap();
892    cx_b.run_until_parked();
893    let active_call_b = cx_b.read(ActiveCall::global);
894    active_call_b
895        .update(cx_b, |call, cx| call.accept_incoming(cx))
896        .await
897        .unwrap();
898    cx_a.run_until_parked();
899
900    let project_b = client_b.join_remote_project(project_id, cx_b).await;
901    cx_a.run_until_parked();
902
903    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
904    let panel_b = workspace_b.update_in(cx_b, GitPanel::new_test);
905    workspace_b.update_in(cx_b, |workspace, window, cx| {
906        workspace.add_panel(panel_b.clone(), window, cx);
907    });
908    cx_b.run_until_parked();
909
910    let stats_b = collect_diff_stats(&panel_b, cx_b);
911    assert_eq!(
912        stats_b, expected_after_edit,
913        "remote diff stats should be restored from the database after rejoining the call"
914    );
915}