1use crate::{FakeFs, FakeFsEntry, Fs, RemoveOptions, RenameOptions};
2use anyhow::{Context as _, Result, bail};
3use collections::{HashMap, HashSet};
4use futures::future::{self, BoxFuture, join_all};
5use git::{
6 Oid, RunHook,
7 blame::Blame,
8 repository::{
9 AskPassDelegate, Branch, CommitDataReader, CommitDetails, CommitOptions, FetchOptions,
10 GRAPH_CHUNK_SIZE, GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder,
11 LogSource, PushOptions, Remote, RepoPath, ResetMode, Worktree,
12 },
13 status::{
14 DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
15 UnmergedStatus,
16 },
17};
18use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
19use ignore::gitignore::GitignoreBuilder;
20use parking_lot::Mutex;
21use rope::Rope;
22use smol::{channel::Sender, future::FutureExt as _};
23use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool};
24use text::LineEnding;
25use util::{paths::PathStyle, rel_path::RelPath};
26
27#[derive(Clone)]
28pub struct FakeGitRepository {
29 pub(crate) fs: Arc<FakeFs>,
30 pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
31 pub(crate) executor: BackgroundExecutor,
32 pub(crate) dot_git_path: PathBuf,
33 pub(crate) repository_dir_path: PathBuf,
34 pub(crate) common_dir_path: PathBuf,
35 pub(crate) is_trusted: Arc<AtomicBool>,
36}
37
38#[derive(Debug, Clone)]
39pub struct FakeGitRepositoryState {
40 pub event_emitter: smol::channel::Sender<PathBuf>,
41 pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
42 pub head_contents: HashMap<RepoPath, String>,
43 pub index_contents: HashMap<RepoPath, String>,
44 // everything in commit contents is in oids
45 pub merge_base_contents: HashMap<RepoPath, Oid>,
46 pub oids: HashMap<Oid, String>,
47 pub blames: HashMap<RepoPath, Blame>,
48 pub current_branch_name: Option<String>,
49 pub branches: HashSet<String>,
50 /// List of remotes, keys are names and values are URLs
51 pub remotes: HashMap<String, String>,
52 pub simulated_index_write_error_message: Option<String>,
53 pub simulated_create_worktree_error: Option<String>,
54 pub refs: HashMap<String, String>,
55 pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
56 pub worktrees: Vec<Worktree>,
57}
58
59impl FakeGitRepositoryState {
60 pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
61 FakeGitRepositoryState {
62 event_emitter,
63 head_contents: Default::default(),
64 index_contents: Default::default(),
65 unmerged_paths: Default::default(),
66 blames: Default::default(),
67 current_branch_name: Default::default(),
68 branches: Default::default(),
69 simulated_index_write_error_message: Default::default(),
70 simulated_create_worktree_error: Default::default(),
71 refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
72 merge_base_contents: Default::default(),
73 oids: Default::default(),
74 remotes: HashMap::default(),
75 graph_commits: Vec::new(),
76 worktrees: Vec::new(),
77 }
78 }
79}
80
81impl FakeGitRepository {
82 fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
83 where
84 F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
85 T: Send,
86 {
87 let fs = self.fs.clone();
88 let executor = self.executor.clone();
89 let dot_git_path = self.dot_git_path.clone();
90 async move {
91 executor.simulate_random_delay().await;
92 fs.with_git_state(&dot_git_path, write, f)?
93 }
94 .boxed()
95 }
96}
97
98impl GitRepository for FakeGitRepository {
99 fn reload_index(&self) {}
100
101 fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
102 let fut = self.with_state_async(false, move |state| {
103 state
104 .index_contents
105 .get(&path)
106 .context("not present in index")
107 .cloned()
108 });
109 self.executor.spawn(async move { fut.await.ok() }).boxed()
110 }
111
112 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
113 let fut = self.with_state_async(false, move |state| {
114 state
115 .head_contents
116 .get(&path)
117 .context("not present in HEAD")
118 .cloned()
119 });
120 self.executor.spawn(async move { fut.await.ok() }).boxed()
121 }
122
123 fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
124 self.with_state_async(false, move |state| {
125 state.oids.get(&oid).cloned().context("oid does not exist")
126 })
127 .boxed()
128 }
129
130 fn load_commit(
131 &self,
132 _commit: String,
133 _cx: AsyncApp,
134 ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
135 unimplemented!()
136 }
137
138 fn set_index_text(
139 &self,
140 path: RepoPath,
141 content: Option<String>,
142 _env: Arc<HashMap<String, String>>,
143 _is_executable: bool,
144 ) -> BoxFuture<'_, anyhow::Result<()>> {
145 self.with_state_async(true, move |state| {
146 if let Some(message) = &state.simulated_index_write_error_message {
147 anyhow::bail!("{message}");
148 } else if let Some(content) = content {
149 state.index_contents.insert(path, content);
150 } else {
151 state.index_contents.remove(&path);
152 }
153 Ok(())
154 })
155 }
156
157 fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
158 let name = name.to_string();
159 let fut = self.with_state_async(false, move |state| {
160 state
161 .remotes
162 .get(&name)
163 .context("remote not found")
164 .cloned()
165 });
166 async move { fut.await.ok() }.boxed()
167 }
168
169 fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
170 let mut entries = HashMap::default();
171 self.with_state_async(false, |state| {
172 for (path, content) in &state.head_contents {
173 let status = if let Some((oid, original)) = state
174 .merge_base_contents
175 .get(path)
176 .map(|oid| (oid, &state.oids[oid]))
177 {
178 if original == content {
179 continue;
180 }
181 TreeDiffStatus::Modified { old: *oid }
182 } else {
183 TreeDiffStatus::Added
184 };
185 entries.insert(path.clone(), status);
186 }
187 for (path, oid) in &state.merge_base_contents {
188 if !entries.contains_key(path) {
189 entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
190 }
191 }
192 Ok(TreeDiff { entries })
193 })
194 .boxed()
195 }
196
197 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
198 self.with_state_async(false, |state| {
199 Ok(revs
200 .into_iter()
201 .map(|rev| state.refs.get(&rev).cloned())
202 .collect())
203 })
204 }
205
206 fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
207 async {
208 Ok(CommitDetails {
209 sha: commit.into(),
210 message: "initial commit".into(),
211 ..Default::default()
212 })
213 }
214 .boxed()
215 }
216
217 fn reset(
218 &self,
219 _commit: String,
220 _mode: ResetMode,
221 _env: Arc<HashMap<String, String>>,
222 ) -> BoxFuture<'_, Result<()>> {
223 unimplemented!()
224 }
225
226 fn checkout_files(
227 &self,
228 _commit: String,
229 _paths: Vec<RepoPath>,
230 _env: Arc<HashMap<String, String>>,
231 ) -> BoxFuture<'_, Result<()>> {
232 unimplemented!()
233 }
234
235 fn path(&self) -> PathBuf {
236 self.repository_dir_path.clone()
237 }
238
239 fn main_repository_path(&self) -> PathBuf {
240 self.common_dir_path.clone()
241 }
242
243 fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
244 async move { None }.boxed()
245 }
246
247 fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
248 let workdir_path = self.dot_git_path.parent().unwrap();
249
250 // Load gitignores
251 let ignores = workdir_path
252 .ancestors()
253 .filter_map(|dir| {
254 let ignore_path = dir.join(".gitignore");
255 let content = self.fs.read_file_sync(ignore_path).ok()?;
256 let content = String::from_utf8(content).ok()?;
257 let mut builder = GitignoreBuilder::new(dir);
258 for line in content.lines() {
259 builder.add_line(Some(dir.into()), line).ok()?;
260 }
261 builder.build().ok()
262 })
263 .collect::<Vec<_>>();
264
265 // Load working copy files.
266 let git_files: HashMap<RepoPath, (String, bool)> = self
267 .fs
268 .files()
269 .iter()
270 .filter_map(|path| {
271 // TODO better simulate git status output in the case of submodules and worktrees
272 let repo_path = path.strip_prefix(workdir_path).ok()?;
273 let mut is_ignored = repo_path.starts_with(".git");
274 for ignore in &ignores {
275 match ignore.matched_path_or_any_parents(path, false) {
276 ignore::Match::None => {}
277 ignore::Match::Ignore(_) => is_ignored = true,
278 ignore::Match::Whitelist(_) => break,
279 }
280 }
281 let content = self
282 .fs
283 .read_file_sync(path)
284 .ok()
285 .map(|content| String::from_utf8(content).unwrap())?;
286 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
287 Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
288 })
289 .collect();
290
291 let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
292 let mut entries = Vec::new();
293 let paths = state
294 .head_contents
295 .keys()
296 .chain(state.index_contents.keys())
297 .chain(git_files.keys())
298 .collect::<HashSet<_>>();
299 for path in paths {
300 if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
301 continue;
302 }
303
304 let head = state.head_contents.get(path);
305 let index = state.index_contents.get(path);
306 let unmerged = state.unmerged_paths.get(path);
307 let fs = git_files.get(path);
308 let status = match (unmerged, head, index, fs) {
309 (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
310 (_, Some(head), Some(index), Some((fs, _))) => {
311 FileStatus::Tracked(TrackedStatus {
312 index_status: if head == index {
313 StatusCode::Unmodified
314 } else {
315 StatusCode::Modified
316 },
317 worktree_status: if fs == index {
318 StatusCode::Unmodified
319 } else {
320 StatusCode::Modified
321 },
322 })
323 }
324 (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
325 index_status: if head == index {
326 StatusCode::Unmodified
327 } else {
328 StatusCode::Modified
329 },
330 worktree_status: StatusCode::Deleted,
331 }),
332 (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
333 index_status: StatusCode::Deleted,
334 worktree_status: StatusCode::Added,
335 }),
336 (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
337 index_status: StatusCode::Deleted,
338 worktree_status: StatusCode::Deleted,
339 }),
340 (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
341 index_status: StatusCode::Added,
342 worktree_status: if fs == index {
343 StatusCode::Unmodified
344 } else {
345 StatusCode::Modified
346 },
347 }),
348 (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
349 index_status: StatusCode::Added,
350 worktree_status: StatusCode::Deleted,
351 }),
352 (_, None, None, Some((_, is_ignored))) => {
353 if *is_ignored {
354 continue;
355 }
356 FileStatus::Untracked
357 }
358 (_, None, None, None) => {
359 unreachable!();
360 }
361 };
362 if status
363 != FileStatus::Tracked(TrackedStatus {
364 index_status: StatusCode::Unmodified,
365 worktree_status: StatusCode::Unmodified,
366 })
367 {
368 entries.push((path.clone(), status));
369 }
370 }
371 entries.sort_by(|a, b| a.0.cmp(&b.0));
372 anyhow::Ok(GitStatus {
373 entries: entries.into(),
374 })
375 });
376 Task::ready(match result {
377 Ok(result) => result,
378 Err(e) => Err(e),
379 })
380 }
381
382 fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
383 async { Ok(git::stash::GitStash::default()) }.boxed()
384 }
385
386 fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
387 self.with_state_async(false, move |state| {
388 let current_branch = &state.current_branch_name;
389 Ok(state
390 .branches
391 .iter()
392 .map(|branch_name| {
393 let ref_name = if branch_name.starts_with("refs/") {
394 branch_name.into()
395 } else if branch_name.contains('/') {
396 format!("refs/remotes/{branch_name}").into()
397 } else {
398 format!("refs/heads/{branch_name}").into()
399 };
400 Branch {
401 is_head: Some(branch_name) == current_branch.as_ref(),
402 ref_name,
403 most_recent_commit: None,
404 upstream: None,
405 }
406 })
407 .collect())
408 })
409 }
410
411 fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
412 let dot_git_path = self.dot_git_path.clone();
413 self.with_state_async(false, move |state| {
414 let work_dir = dot_git_path
415 .parent()
416 .map(PathBuf::from)
417 .unwrap_or(dot_git_path);
418 let head_sha = state
419 .refs
420 .get("HEAD")
421 .cloned()
422 .unwrap_or_else(|| "0000000".to_string());
423 let branch_ref = state
424 .current_branch_name
425 .as_ref()
426 .map(|name| format!("refs/heads/{name}"))
427 .unwrap_or_else(|| "refs/heads/main".to_string());
428 let main_worktree = Worktree {
429 path: work_dir,
430 ref_name: branch_ref.into(),
431 sha: head_sha.into(),
432 };
433 let mut all = vec![main_worktree];
434 all.extend(state.worktrees.iter().cloned());
435 Ok(all)
436 })
437 }
438
439 fn create_worktree(
440 &self,
441 name: String,
442 directory: PathBuf,
443 from_commit: Option<String>,
444 ) -> BoxFuture<'_, Result<()>> {
445 let fs = self.fs.clone();
446 let executor = self.executor.clone();
447 let dot_git_path = self.dot_git_path.clone();
448 async move {
449 let path = directory.join(&name);
450 executor.simulate_random_delay().await;
451 // Check for simulated error before any side effects
452 fs.with_git_state(&dot_git_path, false, |state| {
453 if let Some(message) = &state.simulated_create_worktree_error {
454 anyhow::bail!("{message}");
455 }
456 Ok(())
457 })??;
458 // Create directory before updating state so state is never
459 // inconsistent with the filesystem
460 fs.create_dir(&path).await?;
461 fs.with_git_state(&dot_git_path, true, {
462 let path = path.clone();
463 move |state| {
464 if state.branches.contains(&name) {
465 bail!("a branch named '{}' already exists", name);
466 }
467 let ref_name = format!("refs/heads/{name}");
468 let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
469 state.refs.insert(ref_name.clone(), sha.clone());
470 state.worktrees.push(Worktree {
471 path,
472 ref_name: ref_name.into(),
473 sha: sha.into(),
474 });
475 state.branches.insert(name);
476 Ok::<(), anyhow::Error>(())
477 }
478 })??;
479 Ok(())
480 }
481 .boxed()
482 }
483
484 fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
485 let fs = self.fs.clone();
486 let executor = self.executor.clone();
487 let dot_git_path = self.dot_git_path.clone();
488 async move {
489 executor.simulate_random_delay().await;
490 // Validate the worktree exists in state before touching the filesystem
491 fs.with_git_state(&dot_git_path, false, {
492 let path = path.clone();
493 move |state| {
494 if !state.worktrees.iter().any(|w| w.path == path) {
495 bail!("no worktree found at path: {}", path.display());
496 }
497 Ok(())
498 }
499 })??;
500 // Now remove the directory
501 fs.remove_dir(
502 &path,
503 RemoveOptions {
504 recursive: true,
505 ignore_if_not_exists: false,
506 },
507 )
508 .await?;
509 // Update state
510 fs.with_git_state(&dot_git_path, true, move |state| {
511 state.worktrees.retain(|worktree| worktree.path != path);
512 Ok::<(), anyhow::Error>(())
513 })??;
514 Ok(())
515 }
516 .boxed()
517 }
518
519 fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
520 let fs = self.fs.clone();
521 let executor = self.executor.clone();
522 let dot_git_path = self.dot_git_path.clone();
523 async move {
524 executor.simulate_random_delay().await;
525 // Validate the worktree exists in state before touching the filesystem
526 fs.with_git_state(&dot_git_path, false, {
527 let old_path = old_path.clone();
528 move |state| {
529 if !state.worktrees.iter().any(|w| w.path == old_path) {
530 bail!("no worktree found at path: {}", old_path.display());
531 }
532 Ok(())
533 }
534 })??;
535 // Now move the directory
536 fs.rename(
537 &old_path,
538 &new_path,
539 RenameOptions {
540 overwrite: false,
541 ignore_if_exists: false,
542 create_parents: true,
543 },
544 )
545 .await?;
546 // Update state
547 fs.with_git_state(&dot_git_path, true, move |state| {
548 let worktree = state
549 .worktrees
550 .iter_mut()
551 .find(|worktree| worktree.path == old_path)
552 .expect("worktree was validated above");
553 worktree.path = new_path;
554 Ok::<(), anyhow::Error>(())
555 })??;
556 Ok(())
557 }
558 .boxed()
559 }
560
561 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
562 self.with_state_async(true, |state| {
563 state.current_branch_name = Some(name);
564 Ok(())
565 })
566 }
567
568 fn create_branch(
569 &self,
570 name: String,
571 _base_branch: Option<String>,
572 ) -> BoxFuture<'_, Result<()>> {
573 self.with_state_async(true, move |state| {
574 if let Some((remote, _)) = name.split_once('/')
575 && !state.remotes.contains_key(remote)
576 {
577 state.remotes.insert(remote.to_owned(), "".to_owned());
578 }
579 state.branches.insert(name);
580 Ok(())
581 })
582 }
583
584 fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
585 self.with_state_async(true, move |state| {
586 if !state.branches.remove(&branch) {
587 bail!("no such branch: {branch}");
588 }
589 state.branches.insert(new_name.clone());
590 if state.current_branch_name == Some(branch) {
591 state.current_branch_name = Some(new_name);
592 }
593 Ok(())
594 })
595 }
596
597 fn delete_branch(&self, _is_remote: bool, name: String) -> BoxFuture<'_, Result<()>> {
598 self.with_state_async(true, move |state| {
599 if !state.branches.remove(&name) {
600 bail!("no such branch: {name}");
601 }
602 Ok(())
603 })
604 }
605
606 fn blame(
607 &self,
608 path: RepoPath,
609 _content: Rope,
610 _line_ending: LineEnding,
611 ) -> BoxFuture<'_, Result<git::blame::Blame>> {
612 self.with_state_async(false, move |state| {
613 state
614 .blames
615 .get(&path)
616 .with_context(|| format!("failed to get blame for {:?}", path))
617 .cloned()
618 })
619 }
620
621 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
622 self.file_history_paginated(path, 0, None)
623 }
624
625 fn file_history_paginated(
626 &self,
627 path: RepoPath,
628 _skip: usize,
629 _limit: Option<usize>,
630 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
631 async move {
632 Ok(git::repository::FileHistory {
633 entries: Vec::new(),
634 path,
635 })
636 }
637 .boxed()
638 }
639
640 fn stage_paths(
641 &self,
642 paths: Vec<RepoPath>,
643 _env: Arc<HashMap<String, String>>,
644 ) -> BoxFuture<'_, Result<()>> {
645 Box::pin(async move {
646 let contents = paths
647 .into_iter()
648 .map(|path| {
649 let abs_path = self
650 .dot_git_path
651 .parent()
652 .unwrap()
653 .join(&path.as_std_path());
654 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
655 })
656 .collect::<Vec<_>>();
657 let contents = join_all(contents).await;
658 self.with_state_async(true, move |state| {
659 for (path, content) in contents {
660 if let Some(content) = content {
661 state.index_contents.insert(path, content);
662 } else {
663 state.index_contents.remove(&path);
664 }
665 }
666 Ok(())
667 })
668 .await
669 })
670 }
671
672 fn unstage_paths(
673 &self,
674 paths: Vec<RepoPath>,
675 _env: Arc<HashMap<String, String>>,
676 ) -> BoxFuture<'_, Result<()>> {
677 self.with_state_async(true, move |state| {
678 for path in paths {
679 match state.head_contents.get(&path) {
680 Some(content) => state.index_contents.insert(path, content.clone()),
681 None => state.index_contents.remove(&path),
682 };
683 }
684 Ok(())
685 })
686 }
687
688 fn stash_paths(
689 &self,
690 _paths: Vec<RepoPath>,
691 _env: Arc<HashMap<String, String>>,
692 ) -> BoxFuture<'_, Result<()>> {
693 unimplemented!()
694 }
695
696 fn stash_pop(
697 &self,
698 _index: Option<usize>,
699 _env: Arc<HashMap<String, String>>,
700 ) -> BoxFuture<'_, Result<()>> {
701 unimplemented!()
702 }
703
704 fn stash_apply(
705 &self,
706 _index: Option<usize>,
707 _env: Arc<HashMap<String, String>>,
708 ) -> BoxFuture<'_, Result<()>> {
709 unimplemented!()
710 }
711
712 fn stash_drop(
713 &self,
714 _index: Option<usize>,
715 _env: Arc<HashMap<String, String>>,
716 ) -> BoxFuture<'_, Result<()>> {
717 unimplemented!()
718 }
719
720 fn commit(
721 &self,
722 _message: gpui::SharedString,
723 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
724 _options: CommitOptions,
725 _askpass: AskPassDelegate,
726 _env: Arc<HashMap<String, String>>,
727 ) -> BoxFuture<'_, Result<()>> {
728 async { Ok(()) }.boxed()
729 }
730
731 fn run_hook(
732 &self,
733 _hook: RunHook,
734 _env: Arc<HashMap<String, String>>,
735 ) -> BoxFuture<'_, Result<()>> {
736 async { Ok(()) }.boxed()
737 }
738
739 fn push(
740 &self,
741 _branch: String,
742 _remote_branch: String,
743 _remote: String,
744 _options: Option<PushOptions>,
745 _askpass: AskPassDelegate,
746 _env: Arc<HashMap<String, String>>,
747 _cx: AsyncApp,
748 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
749 unimplemented!()
750 }
751
752 fn pull(
753 &self,
754 _branch: Option<String>,
755 _remote: String,
756 _rebase: bool,
757 _askpass: AskPassDelegate,
758 _env: Arc<HashMap<String, String>>,
759 _cx: AsyncApp,
760 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
761 unimplemented!()
762 }
763
764 fn fetch(
765 &self,
766 _fetch_options: FetchOptions,
767 _askpass: AskPassDelegate,
768 _env: Arc<HashMap<String, String>>,
769 _cx: AsyncApp,
770 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
771 unimplemented!()
772 }
773
774 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
775 self.with_state_async(false, move |state| {
776 let remotes = state
777 .remotes
778 .keys()
779 .map(|r| Remote {
780 name: r.clone().into(),
781 })
782 .collect::<Vec<_>>();
783 Ok(remotes)
784 })
785 }
786
787 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
788 unimplemented!()
789 }
790
791 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
792 unimplemented!()
793 }
794
795 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
796 future::ready(Ok(Vec::new())).boxed()
797 }
798
799 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
800 future::ready(Ok(String::new())).boxed()
801 }
802
803 fn diff_stat(
804 &self,
805 path_prefixes: &[RepoPath],
806 ) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
807 fn count_lines(s: &str) -> u32 {
808 if s.is_empty() {
809 0
810 } else {
811 s.lines().count() as u32
812 }
813 }
814
815 fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
816 if prefixes.is_empty() {
817 return true;
818 }
819 prefixes.iter().any(|prefix| {
820 let prefix_str = prefix.as_unix_str();
821 if prefix_str == "." {
822 return true;
823 }
824 path == prefix || path.starts_with(&prefix)
825 })
826 }
827
828 let path_prefixes = path_prefixes.to_vec();
829
830 let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
831 let worktree_files: HashMap<RepoPath, String> = self
832 .fs
833 .files()
834 .iter()
835 .filter_map(|path| {
836 let repo_path = path.strip_prefix(&workdir_path).ok()?;
837 if repo_path.starts_with(".git") {
838 return None;
839 }
840 let content = self
841 .fs
842 .read_file_sync(path)
843 .ok()
844 .and_then(|bytes| String::from_utf8(bytes).ok())?;
845 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
846 Some((RepoPath::from_rel_path(&repo_path), content))
847 })
848 .collect();
849
850 self.with_state_async(false, move |state| {
851 let mut entries = Vec::new();
852 let all_paths: HashSet<&RepoPath> = state
853 .head_contents
854 .keys()
855 .chain(
856 worktree_files
857 .keys()
858 .filter(|p| state.index_contents.contains_key(*p)),
859 )
860 .collect();
861 for path in all_paths {
862 if !matches_prefixes(path, &path_prefixes) {
863 continue;
864 }
865 let head = state.head_contents.get(path);
866 let worktree = worktree_files.get(path);
867 match (head, worktree) {
868 (Some(old), Some(new)) if old != new => {
869 entries.push((
870 path.clone(),
871 git::status::DiffStat {
872 added: count_lines(new),
873 deleted: count_lines(old),
874 },
875 ));
876 }
877 (Some(old), None) => {
878 entries.push((
879 path.clone(),
880 git::status::DiffStat {
881 added: 0,
882 deleted: count_lines(old),
883 },
884 ));
885 }
886 (None, Some(new)) => {
887 entries.push((
888 path.clone(),
889 git::status::DiffStat {
890 added: count_lines(new),
891 deleted: 0,
892 },
893 ));
894 }
895 _ => {}
896 }
897 }
898 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
899 Ok(git::status::GitDiffStat {
900 entries: entries.into(),
901 })
902 })
903 .boxed()
904 }
905
906 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
907 let executor = self.executor.clone();
908 let fs = self.fs.clone();
909 let checkpoints = self.checkpoints.clone();
910 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
911 async move {
912 executor.simulate_random_delay().await;
913 let oid = git::Oid::random(&mut *executor.rng().lock());
914 let entry = fs.entry(&repository_dir_path)?;
915 checkpoints.lock().insert(oid, entry);
916 Ok(GitRepositoryCheckpoint { commit_sha: oid })
917 }
918 .boxed()
919 }
920
921 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
922 let executor = self.executor.clone();
923 let fs = self.fs.clone();
924 let checkpoints = self.checkpoints.clone();
925 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
926 async move {
927 executor.simulate_random_delay().await;
928 let checkpoints = checkpoints.lock();
929 let entry = checkpoints
930 .get(&checkpoint.commit_sha)
931 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
932 fs.insert_entry(&repository_dir_path, entry.clone())?;
933 Ok(())
934 }
935 .boxed()
936 }
937
938 fn compare_checkpoints(
939 &self,
940 left: GitRepositoryCheckpoint,
941 right: GitRepositoryCheckpoint,
942 ) -> BoxFuture<'_, Result<bool>> {
943 let executor = self.executor.clone();
944 let checkpoints = self.checkpoints.clone();
945 async move {
946 executor.simulate_random_delay().await;
947 let checkpoints = checkpoints.lock();
948 let left = checkpoints
949 .get(&left.commit_sha)
950 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
951 let right = checkpoints
952 .get(&right.commit_sha)
953 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
954
955 Ok(left == right)
956 }
957 .boxed()
958 }
959
960 fn diff_checkpoints(
961 &self,
962 _base_checkpoint: GitRepositoryCheckpoint,
963 _target_checkpoint: GitRepositoryCheckpoint,
964 ) -> BoxFuture<'_, Result<String>> {
965 unimplemented!()
966 }
967
968 fn default_branch(
969 &self,
970 include_remote_name: bool,
971 ) -> BoxFuture<'_, Result<Option<SharedString>>> {
972 async move {
973 Ok(Some(if include_remote_name {
974 "origin/main".into()
975 } else {
976 "main".into()
977 }))
978 }
979 .boxed()
980 }
981
982 fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
983 self.with_state_async(true, move |state| {
984 state.remotes.insert(name, url);
985 Ok(())
986 })
987 }
988
989 fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
990 self.with_state_async(true, move |state| {
991 state.branches.retain(|branch| {
992 branch
993 .split_once('/')
994 .is_none_or(|(remote, _)| remote != name)
995 });
996 state.remotes.remove(&name);
997 Ok(())
998 })
999 }
1000
1001 fn initial_graph_data(
1002 &self,
1003 _log_source: LogSource,
1004 _log_order: LogOrder,
1005 request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
1006 ) -> BoxFuture<'_, Result<()>> {
1007 let fs = self.fs.clone();
1008 let dot_git_path = self.dot_git_path.clone();
1009 async move {
1010 let graph_commits =
1011 fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
1012
1013 for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1014 request_tx.send(chunk.to_vec()).await.ok();
1015 }
1016 Ok(())
1017 }
1018 .boxed()
1019 }
1020
1021 fn commit_data_reader(&self) -> Result<CommitDataReader> {
1022 anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
1023 }
1024
1025 fn set_trusted(&self, trusted: bool) {
1026 self.is_trusted
1027 .store(trusted, std::sync::atomic::Ordering::Release);
1028 }
1029
1030 fn is_trusted(&self) -> bool {
1031 self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1032 }
1033}