git_tests.rs

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