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}