1use crate::FakeFs;
2use anyhow::{anyhow, Context as _, Result};
3use collections::{HashMap, HashSet};
4use futures::future::{self, BoxFuture};
5use git::{
6 blame::Blame,
7 repository::{
8 AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
9 Remote, RepoPath, ResetMode,
10 },
11 status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
12};
13use gpui::{AsyncApp, BackgroundExecutor};
14use ignore::gitignore::GitignoreBuilder;
15use rope::Rope;
16use smol::future::FutureExt as _;
17use std::{path::PathBuf, sync::Arc};
18
19#[derive(Clone)]
20pub struct FakeGitRepository {
21 pub(crate) fs: Arc<FakeFs>,
22 pub(crate) executor: BackgroundExecutor,
23 pub(crate) dot_git_path: PathBuf,
24}
25
26#[derive(Debug, Clone)]
27pub struct FakeGitRepositoryState {
28 pub path: PathBuf,
29 pub event_emitter: smol::channel::Sender<PathBuf>,
30 pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
31 pub head_contents: HashMap<RepoPath, String>,
32 pub index_contents: HashMap<RepoPath, String>,
33 pub blames: HashMap<RepoPath, Blame>,
34 pub current_branch_name: Option<String>,
35 pub branches: HashSet<String>,
36 pub simulated_index_write_error_message: Option<String>,
37}
38
39impl FakeGitRepositoryState {
40 pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
41 FakeGitRepositoryState {
42 path,
43 event_emitter,
44 head_contents: Default::default(),
45 index_contents: Default::default(),
46 unmerged_paths: Default::default(),
47 blames: Default::default(),
48 current_branch_name: Default::default(),
49 branches: Default::default(),
50 simulated_index_write_error_message: Default::default(),
51 }
52 }
53}
54
55impl FakeGitRepository {
56 fn with_state<F, T>(&self, f: F) -> T
57 where
58 F: FnOnce(&mut FakeGitRepositoryState) -> T,
59 {
60 self.fs.with_git_state(&self.dot_git_path, false, f)
61 }
62
63 fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<T>
64 where
65 F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> T,
66 T: Send,
67 {
68 let fs = self.fs.clone();
69 let executor = self.executor.clone();
70 let dot_git_path = self.dot_git_path.clone();
71 async move {
72 executor.simulate_random_delay().await;
73 fs.with_git_state(&dot_git_path, write, f)
74 }
75 .boxed()
76 }
77}
78
79impl GitRepository for FakeGitRepository {
80 fn reload_index(&self) {}
81
82 fn load_index_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
83 self.with_state_async(false, move |state| {
84 state.index_contents.get(path.as_ref()).cloned()
85 })
86 }
87
88 fn load_committed_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
89 self.with_state_async(false, move |state| {
90 state.head_contents.get(path.as_ref()).cloned()
91 })
92 }
93
94 fn set_index_text(
95 &self,
96 path: RepoPath,
97 content: Option<String>,
98 _env: HashMap<String, String>,
99 _cx: AsyncApp,
100 ) -> BoxFuture<anyhow::Result<()>> {
101 self.with_state_async(true, move |state| {
102 if let Some(message) = state.simulated_index_write_error_message.clone() {
103 return Err(anyhow!("{}", message));
104 } else if let Some(content) = content {
105 state.index_contents.insert(path, content);
106 } else {
107 state.index_contents.remove(&path);
108 }
109 Ok(())
110 })
111 }
112
113 fn remote_url(&self, _name: &str) -> Option<String> {
114 None
115 }
116
117 fn head_sha(&self) -> Option<String> {
118 None
119 }
120
121 fn merge_head_shas(&self) -> Vec<String> {
122 vec![]
123 }
124
125 fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
126 unimplemented!()
127 }
128
129 fn reset(
130 &self,
131 _commit: String,
132 _mode: ResetMode,
133 _env: HashMap<String, String>,
134 ) -> BoxFuture<Result<()>> {
135 unimplemented!()
136 }
137
138 fn checkout_files(
139 &self,
140 _commit: String,
141 _paths: Vec<RepoPath>,
142 _env: HashMap<String, String>,
143 ) -> BoxFuture<Result<()>> {
144 unimplemented!()
145 }
146
147 fn path(&self) -> PathBuf {
148 self.with_state(|state| state.path.clone())
149 }
150
151 fn main_repository_path(&self) -> PathBuf {
152 self.path()
153 }
154
155 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
156 let workdir_path = self.dot_git_path.parent().unwrap();
157
158 // Load gitignores
159 let ignores = workdir_path
160 .ancestors()
161 .filter_map(|dir| {
162 let ignore_path = dir.join(".gitignore");
163 let content = self.fs.read_file_sync(ignore_path).ok()?;
164 let content = String::from_utf8(content).ok()?;
165 let mut builder = GitignoreBuilder::new(dir);
166 for line in content.lines() {
167 builder.add_line(Some(dir.into()), line).ok()?;
168 }
169 builder.build().ok()
170 })
171 .collect::<Vec<_>>();
172
173 // Load working copy files.
174 let git_files: HashMap<RepoPath, (String, bool)> = self
175 .fs
176 .files()
177 .iter()
178 .filter_map(|path| {
179 let repo_path = path.strip_prefix(workdir_path).ok()?;
180 let mut is_ignored = false;
181 for ignore in &ignores {
182 match ignore.matched_path_or_any_parents(path, false) {
183 ignore::Match::None => {}
184 ignore::Match::Ignore(_) => is_ignored = true,
185 ignore::Match::Whitelist(_) => break,
186 }
187 }
188 let content = self
189 .fs
190 .read_file_sync(path)
191 .ok()
192 .map(|content| String::from_utf8(content).unwrap())?;
193 Some((repo_path.into(), (content, is_ignored)))
194 })
195 .collect();
196
197 self.with_state(|state| {
198 let mut entries = Vec::new();
199 let paths = state
200 .head_contents
201 .keys()
202 .chain(state.index_contents.keys())
203 .chain(git_files.keys())
204 .collect::<HashSet<_>>();
205 for path in paths {
206 if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
207 continue;
208 }
209
210 let head = state.head_contents.get(path);
211 let index = state.index_contents.get(path);
212 let unmerged = state.unmerged_paths.get(path);
213 let fs = git_files.get(path);
214 let status = match (unmerged, head, index, fs) {
215 (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
216 (_, Some(head), Some(index), Some((fs, _))) => {
217 FileStatus::Tracked(TrackedStatus {
218 index_status: if head == index {
219 StatusCode::Unmodified
220 } else {
221 StatusCode::Modified
222 },
223 worktree_status: if fs == index {
224 StatusCode::Unmodified
225 } else {
226 StatusCode::Modified
227 },
228 })
229 }
230 (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
231 index_status: if head == index {
232 StatusCode::Unmodified
233 } else {
234 StatusCode::Modified
235 },
236 worktree_status: StatusCode::Deleted,
237 }),
238 (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
239 index_status: StatusCode::Deleted,
240 worktree_status: StatusCode::Added,
241 }),
242 (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
243 index_status: StatusCode::Deleted,
244 worktree_status: StatusCode::Deleted,
245 }),
246 (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
247 index_status: StatusCode::Added,
248 worktree_status: if fs == index {
249 StatusCode::Unmodified
250 } else {
251 StatusCode::Modified
252 },
253 }),
254 (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
255 index_status: StatusCode::Added,
256 worktree_status: StatusCode::Deleted,
257 }),
258 (_, None, None, Some((_, is_ignored))) => {
259 if *is_ignored {
260 continue;
261 }
262 FileStatus::Untracked
263 }
264 (_, None, None, None) => {
265 unreachable!();
266 }
267 };
268 if status
269 != FileStatus::Tracked(TrackedStatus {
270 index_status: StatusCode::Unmodified,
271 worktree_status: StatusCode::Unmodified,
272 })
273 {
274 entries.push((path.clone(), status));
275 }
276 }
277 entries.sort_by(|a, b| a.0.cmp(&b.0));
278 Ok(GitStatus {
279 entries: entries.into(),
280 })
281 })
282 }
283
284 fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
285 self.with_state_async(false, move |state| {
286 let current_branch = &state.current_branch_name;
287 Ok(state
288 .branches
289 .iter()
290 .map(|branch_name| Branch {
291 is_head: Some(branch_name) == current_branch.as_ref(),
292 name: branch_name.into(),
293 most_recent_commit: None,
294 upstream: None,
295 })
296 .collect())
297 })
298 }
299
300 fn change_branch(&self, name: String, _cx: AsyncApp) -> BoxFuture<Result<()>> {
301 self.with_state_async(true, |state| {
302 state.current_branch_name = Some(name);
303 Ok(())
304 })
305 }
306
307 fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
308 self.with_state_async(true, move |state| {
309 state.branches.insert(name.to_owned());
310 Ok(())
311 })
312 }
313
314 fn blame(
315 &self,
316 path: RepoPath,
317 _content: Rope,
318 _cx: &mut AsyncApp,
319 ) -> BoxFuture<Result<git::blame::Blame>> {
320 self.with_state_async(false, move |state| {
321 state
322 .blames
323 .get(&path)
324 .with_context(|| format!("failed to get blame for {:?}", path.0))
325 .cloned()
326 })
327 }
328
329 fn stage_paths(
330 &self,
331 _paths: Vec<RepoPath>,
332 _env: HashMap<String, String>,
333 _cx: AsyncApp,
334 ) -> BoxFuture<Result<()>> {
335 unimplemented!()
336 }
337
338 fn unstage_paths(
339 &self,
340 _paths: Vec<RepoPath>,
341 _env: HashMap<String, String>,
342 _cx: AsyncApp,
343 ) -> BoxFuture<Result<()>> {
344 unimplemented!()
345 }
346
347 fn commit(
348 &self,
349 _message: gpui::SharedString,
350 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
351 _env: HashMap<String, String>,
352 _cx: AsyncApp,
353 ) -> BoxFuture<Result<()>> {
354 unimplemented!()
355 }
356
357 fn push(
358 &self,
359 _branch: String,
360 _remote: String,
361 _options: Option<PushOptions>,
362 _askpass: AskPassSession,
363 _env: HashMap<String, String>,
364 _cx: AsyncApp,
365 ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
366 unimplemented!()
367 }
368
369 fn pull(
370 &self,
371 _branch: String,
372 _remote: String,
373 _askpass: AskPassSession,
374 _env: HashMap<String, String>,
375 _cx: AsyncApp,
376 ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
377 unimplemented!()
378 }
379
380 fn fetch(
381 &self,
382 _askpass: AskPassSession,
383 _env: HashMap<String, String>,
384 _cx: AsyncApp,
385 ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
386 unimplemented!()
387 }
388
389 fn get_remotes(
390 &self,
391 _branch: Option<String>,
392 _cx: AsyncApp,
393 ) -> BoxFuture<Result<Vec<Remote>>> {
394 unimplemented!()
395 }
396
397 fn check_for_pushed_commit(
398 &self,
399 _cx: gpui::AsyncApp,
400 ) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
401 future::ready(Ok(Vec::new())).boxed()
402 }
403
404 fn diff(
405 &self,
406 _diff: git::repository::DiffType,
407 _cx: gpui::AsyncApp,
408 ) -> BoxFuture<Result<String>> {
409 unimplemented!()
410 }
411
412 fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
413 unimplemented!()
414 }
415
416 fn restore_checkpoint(
417 &self,
418 _checkpoint: GitRepositoryCheckpoint,
419 _cx: AsyncApp,
420 ) -> BoxFuture<Result<()>> {
421 unimplemented!()
422 }
423
424 fn compare_checkpoints(
425 &self,
426 _left: GitRepositoryCheckpoint,
427 _right: GitRepositoryCheckpoint,
428 _cx: AsyncApp,
429 ) -> BoxFuture<Result<bool>> {
430 unimplemented!()
431 }
432
433 fn delete_checkpoint(
434 &self,
435 _checkpoint: GitRepositoryCheckpoint,
436 _cx: AsyncApp,
437 ) -> BoxFuture<Result<()>> {
438 unimplemented!()
439 }
440}