1use fs::{FakeFs, Fs};
2use gpui::{BackgroundExecutor, TestAppContext};
3use serde_json::json;
4use std::path::{Path, PathBuf};
5use util::path;
6
7#[gpui::test]
8async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
9 let fs = FakeFs::new(cx.executor());
10 fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"}))
11 .await;
12 let repo = fs
13 .open_repo(Path::new("/project/.git"), None)
14 .expect("should open fake repo");
15
16 // Initially only the main worktree exists
17 let worktrees = repo.worktrees().await.unwrap();
18 assert_eq!(worktrees.len(), 1);
19 assert_eq!(worktrees[0].path, PathBuf::from("/project"));
20
21 fs.create_dir("/my-worktrees".as_ref()).await.unwrap();
22 let worktrees_dir = Path::new("/my-worktrees");
23
24 // Create a worktree
25 let worktree_1_dir = worktrees_dir.join("feature-branch");
26 repo.create_worktree(
27 Some("feature-branch".to_string()),
28 worktree_1_dir.clone(),
29 Some("abc123".to_string()),
30 )
31 .await
32 .unwrap();
33
34 // List worktrees — should have main + one created
35 let worktrees = repo.worktrees().await.unwrap();
36 assert_eq!(worktrees.len(), 2);
37 assert_eq!(worktrees[0].path, PathBuf::from("/project"));
38 assert_eq!(worktrees[1].path, worktree_1_dir);
39 assert_eq!(
40 worktrees[1].ref_name,
41 Some("refs/heads/feature-branch".into())
42 );
43 assert_eq!(worktrees[1].sha.as_ref(), "abc123");
44
45 // Directory should exist in FakeFs after create
46 assert!(fs.is_dir(&worktrees_dir.join("feature-branch")).await);
47
48 // Create a second worktree (without explicit commit)
49 let worktree_2_dir = worktrees_dir.join("bugfix-branch");
50 repo.create_worktree(
51 Some("bugfix-branch".to_string()),
52 worktree_2_dir.clone(),
53 None,
54 )
55 .await
56 .unwrap();
57
58 let worktrees = repo.worktrees().await.unwrap();
59 assert_eq!(worktrees.len(), 3);
60 assert!(fs.is_dir(&worktree_2_dir).await);
61
62 // Rename the first worktree
63 repo.rename_worktree(worktree_1_dir, worktrees_dir.join("renamed-branch"))
64 .await
65 .unwrap();
66
67 let worktrees = repo.worktrees().await.unwrap();
68 assert_eq!(worktrees.len(), 3);
69 assert!(
70 worktrees
71 .iter()
72 .any(|w| w.path == worktrees_dir.join("renamed-branch")),
73 );
74 assert!(
75 worktrees
76 .iter()
77 .all(|w| w.path != worktrees_dir.join("feature-branch")),
78 );
79
80 // Directory should be moved in FakeFs after rename
81 assert!(!fs.is_dir(&worktrees_dir.join("feature-branch")).await);
82 assert!(fs.is_dir(&worktrees_dir.join("renamed-branch")).await);
83
84 // Rename a nonexistent worktree should fail
85 let result = repo
86 .rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere"))
87 .await;
88 assert!(result.is_err());
89
90 // Remove a worktree
91 repo.remove_worktree(worktrees_dir.join("renamed-branch"), false)
92 .await
93 .unwrap();
94
95 let worktrees = repo.worktrees().await.unwrap();
96 assert_eq!(worktrees.len(), 2);
97 assert_eq!(worktrees[0].path, PathBuf::from("/project"));
98 assert_eq!(worktrees[1].path, worktree_2_dir);
99
100 // Directory should be removed from FakeFs after remove
101 assert!(!fs.is_dir(&worktrees_dir.join("renamed-branch")).await);
102
103 // Remove a nonexistent worktree should fail
104 let result = repo
105 .remove_worktree(PathBuf::from("/nonexistent"), false)
106 .await;
107 assert!(result.is_err());
108
109 // Remove the last worktree
110 repo.remove_worktree(worktree_2_dir.clone(), false)
111 .await
112 .unwrap();
113
114 let worktrees = repo.worktrees().await.unwrap();
115 assert_eq!(worktrees.len(), 1);
116 assert_eq!(worktrees[0].path, PathBuf::from("/project"));
117 assert!(!fs.is_dir(&worktree_2_dir).await);
118}
119
120#[gpui::test]
121async fn test_checkpoints(executor: BackgroundExecutor) {
122 let fs = FakeFs::new(executor);
123 fs.insert_tree(
124 path!("/"),
125 json!({
126 "bar": {
127 "baz": "qux"
128 },
129 "foo": {
130 ".git": {},
131 "a": "lorem",
132 "b": "ipsum",
133 },
134 }),
135 )
136 .await;
137 fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
138 .unwrap();
139 let repository = fs
140 .open_repo(Path::new("/foo/.git"), Some("git".as_ref()))
141 .unwrap();
142
143 let checkpoint_1 = repository.checkpoint().await.unwrap();
144 fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
145 fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
146 let checkpoint_2 = repository.checkpoint().await.unwrap();
147 let checkpoint_3 = repository.checkpoint().await.unwrap();
148
149 assert!(
150 repository
151 .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
152 .await
153 .unwrap()
154 );
155 assert!(
156 !repository
157 .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
158 .await
159 .unwrap()
160 );
161
162 repository
163 .restore_checkpoint(checkpoint_1.clone())
164 .await
165 .unwrap();
166 assert_eq!(
167 fs.files_with_contents(Path::new("")),
168 [
169 (Path::new(path!("/bar/baz")).into(), b"qux".into()),
170 (Path::new(path!("/foo/a")).into(), b"lorem".into()),
171 (Path::new(path!("/foo/b")).into(), b"ipsum".into())
172 ]
173 );
174
175 // diff_checkpoints: identical checkpoints produce empty diff
176 let diff = repository
177 .diff_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
178 .await
179 .unwrap();
180 assert!(
181 diff.is_empty(),
182 "identical checkpoints should produce empty diff"
183 );
184
185 // diff_checkpoints: different checkpoints produce non-empty diff
186 let diff = repository
187 .diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
188 .await
189 .unwrap();
190 assert!(diff.contains("b"), "diff should mention changed file 'b'");
191 assert!(diff.contains("c"), "diff should mention added file 'c'");
192}