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