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