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