git_tests.rs

  1use std::path::{Path, PathBuf};
  2
  3use call::ActiveCall;
  4use git::status::{FileStatus, StatusCode, TrackedStatus};
  5use git_ui::project_diff::ProjectDiff;
  6use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, VisualTestContext};
  7use project::ProjectPath;
  8use serde_json::json;
  9use util::{path, rel_path::rel_path};
 10use workspace::{MultiWorkspace, Workspace};
 11
 12use crate::TestServer;
 13
 14#[gpui::test]
 15async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 16    let mut server = TestServer::start(cx_a.background_executor.clone()).await;
 17    let client_a = server.create_client(cx_a, "user_a").await;
 18    let client_b = server.create_client(cx_b, "user_b").await;
 19    cx_a.set_name("cx_a");
 20    cx_b.set_name("cx_b");
 21
 22    server
 23        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 24        .await;
 25
 26    client_a
 27        .fs()
 28        .insert_tree(
 29            path!("/a"),
 30            json!({
 31                ".git": {},
 32                "changed.txt": "after\n",
 33                "unchanged.txt": "unchanged\n",
 34                "created.txt": "created\n",
 35                "secret.pem": "secret-changed\n",
 36            }),
 37        )
 38        .await;
 39
 40    client_a.fs().set_head_and_index_for_repo(
 41        Path::new(path!("/a/.git")),
 42        &[
 43            ("changed.txt", "before\n".to_string()),
 44            ("unchanged.txt", "unchanged\n".to_string()),
 45            ("deleted.txt", "deleted\n".to_string()),
 46            ("secret.pem", "shh\n".to_string()),
 47        ],
 48    );
 49    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
 50    let active_call_a = cx_a.read(ActiveCall::global);
 51    let project_id = active_call_a
 52        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 53        .await
 54        .unwrap();
 55
 56    cx_b.update(editor::init);
 57    cx_b.update(git_ui::init);
 58    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 59    let window_b = cx_b.add_window(|window, cx| {
 60        let workspace = cx.new(|cx| {
 61            Workspace::new(
 62                None,
 63                project_b.clone(),
 64                client_b.app_state.clone(),
 65                window,
 66                cx,
 67            )
 68        });
 69        MultiWorkspace::new(workspace, window, cx)
 70    });
 71    let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
 72    let workspace_b = window_b
 73        .root(cx_b)
 74        .unwrap()
 75        .read_with(cx_b, |multi_workspace, _| {
 76            multi_workspace.workspace().clone()
 77        });
 78
 79    cx_b.update(|window, cx| {
 80        window
 81            .focused(cx)
 82            .unwrap()
 83            .dispatch_action(&git_ui::project_diff::Diff, window, cx)
 84    });
 85    let diff = workspace_b.update(cx_b, |workspace, cx| {
 86        workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx)
 87    });
 88    let diff = diff.unwrap();
 89    cx_b.run_until_parked();
 90
 91    diff.update(cx_b, |diff, cx| {
 92        assert_eq!(
 93            diff.excerpt_paths(cx),
 94            vec![
 95                rel_path("changed.txt").into_arc(),
 96                rel_path("deleted.txt").into_arc(),
 97                rel_path("created.txt").into_arc()
 98            ]
 99        );
100    });
101
102    client_a
103        .fs()
104        .insert_tree(
105            path!("/a"),
106            json!({
107                ".git": {},
108                "changed.txt": "before\n",
109                "unchanged.txt": "changed\n",
110                "created.txt": "created\n",
111                "secret.pem": "secret-changed\n",
112            }),
113        )
114        .await;
115    cx_b.run_until_parked();
116
117    project_b.update(cx_b, |project, cx| {
118        let project_path = ProjectPath {
119            worktree_id,
120            path: rel_path("unchanged.txt").into(),
121        };
122        let status = project.project_path_git_status(&project_path, cx);
123        assert_eq!(
124            status.unwrap(),
125            FileStatus::Tracked(TrackedStatus {
126                worktree_status: StatusCode::Modified,
127                index_status: StatusCode::Unmodified,
128            })
129        );
130    });
131
132    diff.update(cx_b, |diff, cx| {
133        assert_eq!(
134            diff.excerpt_paths(cx),
135            vec![
136                rel_path("deleted.txt").into_arc(),
137                rel_path("unchanged.txt").into_arc(),
138                rel_path("created.txt").into_arc()
139            ]
140        );
141    });
142}
143
144#[gpui::test]
145async fn test_remote_git_worktrees(
146    executor: BackgroundExecutor,
147    cx_a: &mut TestAppContext,
148    cx_b: &mut TestAppContext,
149) {
150    let mut server = TestServer::start(executor.clone()).await;
151    let client_a = server.create_client(cx_a, "user_a").await;
152    let client_b = server.create_client(cx_b, "user_b").await;
153    server
154        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
155        .await;
156    let active_call_a = cx_a.read(ActiveCall::global);
157
158    client_a
159        .fs()
160        .insert_tree(
161            path!("/project"),
162            json!({ ".git": {}, "file.txt": "content" }),
163        )
164        .await;
165
166    let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
167
168    let project_id = active_call_a
169        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
170        .await
171        .unwrap();
172    let project_b = client_b.join_remote_project(project_id, cx_b).await;
173
174    executor.run_until_parked();
175
176    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
177
178    // Initially only the main worktree (the repo itself) should be present
179    let worktrees = cx_b
180        .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
181        .await
182        .unwrap()
183        .unwrap();
184    assert_eq!(worktrees.len(), 1);
185    assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
186
187    // Client B creates a git worktree via the remote project
188    let worktree_directory = PathBuf::from(path!("/project"));
189    cx_b.update(|cx| {
190        repo_b.update(cx, |repository, _| {
191            repository.create_worktree(
192                "feature-branch".to_string(),
193                worktree_directory.clone(),
194                Some("abc123".to_string()),
195            )
196        })
197    })
198    .await
199    .unwrap()
200    .unwrap();
201
202    executor.run_until_parked();
203
204    // Client B lists worktrees — should see main + the one just created
205    let worktrees = cx_b
206        .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
207        .await
208        .unwrap()
209        .unwrap();
210    assert_eq!(worktrees.len(), 2);
211    assert_eq!(worktrees[0].path, PathBuf::from(path!("/project")));
212    assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
213    assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
214    assert_eq!(worktrees[1].sha.as_ref(), "abc123");
215
216    // Verify from the host side that the worktree was actually created
217    let host_worktrees = {
218        let repo_a = cx_a.update(|cx| {
219            project_a
220                .read(cx)
221                .repositories(cx)
222                .values()
223                .next()
224                .unwrap()
225                .clone()
226        });
227        cx_a.update(|cx| repo_a.update(cx, |repository, _| repository.worktrees()))
228            .await
229            .unwrap()
230            .unwrap()
231    };
232    assert_eq!(host_worktrees.len(), 2);
233    assert_eq!(host_worktrees[0].path, PathBuf::from(path!("/project")));
234    assert_eq!(
235        host_worktrees[1].path,
236        worktree_directory.join("feature-branch")
237    );
238
239    // Client B creates a second git worktree without an explicit commit
240    cx_b.update(|cx| {
241        repo_b.update(cx, |repository, _| {
242            repository.create_worktree(
243                "bugfix-branch".to_string(),
244                worktree_directory.clone(),
245                None,
246            )
247        })
248    })
249    .await
250    .unwrap()
251    .unwrap();
252
253    executor.run_until_parked();
254
255    // Client B lists worktrees — should now have main + two created
256    let worktrees = cx_b
257        .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees()))
258        .await
259        .unwrap()
260        .unwrap();
261    assert_eq!(worktrees.len(), 3);
262
263    let feature_worktree = worktrees
264        .iter()
265        .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch")
266        .expect("should find feature-branch worktree");
267    assert_eq!(
268        feature_worktree.path,
269        worktree_directory.join("feature-branch")
270    );
271
272    let bugfix_worktree = worktrees
273        .iter()
274        .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch")
275        .expect("should find bugfix-branch worktree");
276    assert_eq!(
277        bugfix_worktree.path,
278        worktree_directory.join("bugfix-branch")
279    );
280    assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha");
281}