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 {
396 format!("refs/heads/{branch_name}").into()
397 };
398 Branch {
399 is_head: Some(branch_name) == current_branch.as_ref(),
400 ref_name,
401 most_recent_commit: None,
402 upstream: None,
403 }
404 })
405 .collect())
406 })
407 }
408
409 fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
410 let dot_git_path = self.dot_git_path.clone();
411 self.with_state_async(false, move |state| {
412 let work_dir = dot_git_path
413 .parent()
414 .map(PathBuf::from)
415 .unwrap_or(dot_git_path);
416 let head_sha = state
417 .refs
418 .get("HEAD")
419 .cloned()
420 .unwrap_or_else(|| "0000000".to_string());
421 let branch_ref = state
422 .current_branch_name
423 .as_ref()
424 .map(|name| format!("refs/heads/{name}"))
425 .unwrap_or_else(|| "refs/heads/main".to_string());
426 let main_worktree = Worktree {
427 path: work_dir,
428 ref_name: branch_ref.into(),
429 sha: head_sha.into(),
430 };
431 let mut all = vec![main_worktree];
432 all.extend(state.worktrees.iter().cloned());
433 Ok(all)
434 })
435 }
436
437 fn create_worktree(
438 &self,
439 name: String,
440 directory: PathBuf,
441 from_commit: Option<String>,
442 ) -> BoxFuture<'_, Result<()>> {
443 let fs = self.fs.clone();
444 let executor = self.executor.clone();
445 let dot_git_path = self.dot_git_path.clone();
446 async move {
447 let path = directory.join(&name);
448 executor.simulate_random_delay().await;
449 // Check for simulated error before any side effects
450 fs.with_git_state(&dot_git_path, false, |state| {
451 if let Some(message) = &state.simulated_create_worktree_error {
452 anyhow::bail!("{message}");
453 }
454 Ok(())
455 })??;
456 // Create directory before updating state so state is never
457 // inconsistent with the filesystem
458 fs.create_dir(&path).await?;
459 fs.with_git_state(&dot_git_path, true, {
460 let path = path.clone();
461 move |state| {
462 if state.branches.contains(&name) {
463 bail!("a branch named '{}' already exists", name);
464 }
465 let ref_name = format!("refs/heads/{name}");
466 let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
467 state.refs.insert(ref_name.clone(), sha.clone());
468 state.worktrees.push(Worktree {
469 path,
470 ref_name: ref_name.into(),
471 sha: sha.into(),
472 });
473 state.branches.insert(name);
474 Ok::<(), anyhow::Error>(())
475 }
476 })??;
477 Ok(())
478 }
479 .boxed()
480 }
481
482 fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
483 let fs = self.fs.clone();
484 let executor = self.executor.clone();
485 let dot_git_path = self.dot_git_path.clone();
486 async move {
487 executor.simulate_random_delay().await;
488 // Validate the worktree exists in state before touching the filesystem
489 fs.with_git_state(&dot_git_path, false, {
490 let path = path.clone();
491 move |state| {
492 if !state.worktrees.iter().any(|w| w.path == path) {
493 bail!("no worktree found at path: {}", path.display());
494 }
495 Ok(())
496 }
497 })??;
498 // Now remove the directory
499 fs.remove_dir(
500 &path,
501 RemoveOptions {
502 recursive: true,
503 ignore_if_not_exists: false,
504 },
505 )
506 .await?;
507 // Update state
508 fs.with_git_state(&dot_git_path, true, move |state| {
509 state.worktrees.retain(|worktree| worktree.path != path);
510 Ok::<(), anyhow::Error>(())
511 })??;
512 Ok(())
513 }
514 .boxed()
515 }
516
517 fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
518 let fs = self.fs.clone();
519 let executor = self.executor.clone();
520 let dot_git_path = self.dot_git_path.clone();
521 async move {
522 executor.simulate_random_delay().await;
523 // Validate the worktree exists in state before touching the filesystem
524 fs.with_git_state(&dot_git_path, false, {
525 let old_path = old_path.clone();
526 move |state| {
527 if !state.worktrees.iter().any(|w| w.path == old_path) {
528 bail!("no worktree found at path: {}", old_path.display());
529 }
530 Ok(())
531 }
532 })??;
533 // Now move the directory
534 fs.rename(
535 &old_path,
536 &new_path,
537 RenameOptions {
538 overwrite: false,
539 ignore_if_exists: false,
540 create_parents: true,
541 },
542 )
543 .await?;
544 // Update state
545 fs.with_git_state(&dot_git_path, true, move |state| {
546 let worktree = state
547 .worktrees
548 .iter_mut()
549 .find(|worktree| worktree.path == old_path)
550 .expect("worktree was validated above");
551 worktree.path = new_path;
552 Ok::<(), anyhow::Error>(())
553 })??;
554 Ok(())
555 }
556 .boxed()
557 }
558
559 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
560 self.with_state_async(true, |state| {
561 state.current_branch_name = Some(name);
562 Ok(())
563 })
564 }
565
566 fn create_branch(
567 &self,
568 name: String,
569 _base_branch: Option<String>,
570 ) -> BoxFuture<'_, Result<()>> {
571 self.with_state_async(true, move |state| {
572 state.branches.insert(name);
573 Ok(())
574 })
575 }
576
577 fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
578 self.with_state_async(true, move |state| {
579 if !state.branches.remove(&branch) {
580 bail!("no such branch: {branch}");
581 }
582 state.branches.insert(new_name.clone());
583 if state.current_branch_name == Some(branch) {
584 state.current_branch_name = Some(new_name);
585 }
586 Ok(())
587 })
588 }
589
590 fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
591 self.with_state_async(true, move |state| {
592 if !state.branches.remove(&name) {
593 bail!("no such branch: {name}");
594 }
595 Ok(())
596 })
597 }
598
599 fn blame(
600 &self,
601 path: RepoPath,
602 _content: Rope,
603 _line_ending: LineEnding,
604 ) -> BoxFuture<'_, Result<git::blame::Blame>> {
605 self.with_state_async(false, move |state| {
606 state
607 .blames
608 .get(&path)
609 .with_context(|| format!("failed to get blame for {:?}", path))
610 .cloned()
611 })
612 }
613
614 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
615 self.file_history_paginated(path, 0, None)
616 }
617
618 fn file_history_paginated(
619 &self,
620 path: RepoPath,
621 _skip: usize,
622 _limit: Option<usize>,
623 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
624 async move {
625 Ok(git::repository::FileHistory {
626 entries: Vec::new(),
627 path,
628 })
629 }
630 .boxed()
631 }
632
633 fn stage_paths(
634 &self,
635 paths: Vec<RepoPath>,
636 _env: Arc<HashMap<String, String>>,
637 ) -> BoxFuture<'_, Result<()>> {
638 Box::pin(async move {
639 let contents = paths
640 .into_iter()
641 .map(|path| {
642 let abs_path = self
643 .dot_git_path
644 .parent()
645 .unwrap()
646 .join(&path.as_std_path());
647 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
648 })
649 .collect::<Vec<_>>();
650 let contents = join_all(contents).await;
651 self.with_state_async(true, move |state| {
652 for (path, content) in contents {
653 if let Some(content) = content {
654 state.index_contents.insert(path, content);
655 } else {
656 state.index_contents.remove(&path);
657 }
658 }
659 Ok(())
660 })
661 .await
662 })
663 }
664
665 fn unstage_paths(
666 &self,
667 paths: Vec<RepoPath>,
668 _env: Arc<HashMap<String, String>>,
669 ) -> BoxFuture<'_, Result<()>> {
670 self.with_state_async(true, move |state| {
671 for path in paths {
672 match state.head_contents.get(&path) {
673 Some(content) => state.index_contents.insert(path, content.clone()),
674 None => state.index_contents.remove(&path),
675 };
676 }
677 Ok(())
678 })
679 }
680
681 fn stash_paths(
682 &self,
683 _paths: Vec<RepoPath>,
684 _env: Arc<HashMap<String, String>>,
685 ) -> BoxFuture<'_, Result<()>> {
686 unimplemented!()
687 }
688
689 fn stash_pop(
690 &self,
691 _index: Option<usize>,
692 _env: Arc<HashMap<String, String>>,
693 ) -> BoxFuture<'_, Result<()>> {
694 unimplemented!()
695 }
696
697 fn stash_apply(
698 &self,
699 _index: Option<usize>,
700 _env: Arc<HashMap<String, String>>,
701 ) -> BoxFuture<'_, Result<()>> {
702 unimplemented!()
703 }
704
705 fn stash_drop(
706 &self,
707 _index: Option<usize>,
708 _env: Arc<HashMap<String, String>>,
709 ) -> BoxFuture<'_, Result<()>> {
710 unimplemented!()
711 }
712
713 fn commit(
714 &self,
715 _message: gpui::SharedString,
716 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
717 _options: CommitOptions,
718 _askpass: AskPassDelegate,
719 _env: Arc<HashMap<String, String>>,
720 ) -> BoxFuture<'_, Result<()>> {
721 async { Ok(()) }.boxed()
722 }
723
724 fn run_hook(
725 &self,
726 _hook: RunHook,
727 _env: Arc<HashMap<String, String>>,
728 ) -> BoxFuture<'_, Result<()>> {
729 async { Ok(()) }.boxed()
730 }
731
732 fn push(
733 &self,
734 _branch: String,
735 _remote_branch: String,
736 _remote: String,
737 _options: Option<PushOptions>,
738 _askpass: AskPassDelegate,
739 _env: Arc<HashMap<String, String>>,
740 _cx: AsyncApp,
741 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
742 unimplemented!()
743 }
744
745 fn pull(
746 &self,
747 _branch: Option<String>,
748 _remote: String,
749 _rebase: bool,
750 _askpass: AskPassDelegate,
751 _env: Arc<HashMap<String, String>>,
752 _cx: AsyncApp,
753 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
754 unimplemented!()
755 }
756
757 fn fetch(
758 &self,
759 _fetch_options: FetchOptions,
760 _askpass: AskPassDelegate,
761 _env: Arc<HashMap<String, String>>,
762 _cx: AsyncApp,
763 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
764 unimplemented!()
765 }
766
767 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
768 self.with_state_async(false, move |state| {
769 let remotes = state
770 .remotes
771 .keys()
772 .map(|r| Remote {
773 name: r.clone().into(),
774 })
775 .collect::<Vec<_>>();
776 Ok(remotes)
777 })
778 }
779
780 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
781 unimplemented!()
782 }
783
784 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
785 unimplemented!()
786 }
787
788 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
789 future::ready(Ok(Vec::new())).boxed()
790 }
791
792 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
793 unimplemented!()
794 }
795
796 fn diff_stat(
797 &self,
798 path_prefixes: &[RepoPath],
799 ) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
800 fn count_lines(s: &str) -> u32 {
801 if s.is_empty() {
802 0
803 } else {
804 s.lines().count() as u32
805 }
806 }
807
808 fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
809 if prefixes.is_empty() {
810 return true;
811 }
812 prefixes.iter().any(|prefix| {
813 let prefix_str = prefix.as_unix_str();
814 if prefix_str == "." {
815 return true;
816 }
817 path == prefix || path.starts_with(&prefix)
818 })
819 }
820
821 let path_prefixes = path_prefixes.to_vec();
822
823 let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
824 let worktree_files: HashMap<RepoPath, String> = self
825 .fs
826 .files()
827 .iter()
828 .filter_map(|path| {
829 let repo_path = path.strip_prefix(&workdir_path).ok()?;
830 if repo_path.starts_with(".git") {
831 return None;
832 }
833 let content = self
834 .fs
835 .read_file_sync(path)
836 .ok()
837 .and_then(|bytes| String::from_utf8(bytes).ok())?;
838 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
839 Some((RepoPath::from_rel_path(&repo_path), content))
840 })
841 .collect();
842
843 self.with_state_async(false, move |state| {
844 let mut entries = Vec::new();
845 let all_paths: HashSet<&RepoPath> = state
846 .head_contents
847 .keys()
848 .chain(
849 worktree_files
850 .keys()
851 .filter(|p| state.index_contents.contains_key(*p)),
852 )
853 .collect();
854 for path in all_paths {
855 if !matches_prefixes(path, &path_prefixes) {
856 continue;
857 }
858 let head = state.head_contents.get(path);
859 let worktree = worktree_files.get(path);
860 match (head, worktree) {
861 (Some(old), Some(new)) if old != new => {
862 entries.push((
863 path.clone(),
864 git::status::DiffStat {
865 added: count_lines(new),
866 deleted: count_lines(old),
867 },
868 ));
869 }
870 (Some(old), None) => {
871 entries.push((
872 path.clone(),
873 git::status::DiffStat {
874 added: 0,
875 deleted: count_lines(old),
876 },
877 ));
878 }
879 (None, Some(new)) => {
880 entries.push((
881 path.clone(),
882 git::status::DiffStat {
883 added: count_lines(new),
884 deleted: 0,
885 },
886 ));
887 }
888 _ => {}
889 }
890 }
891 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
892 Ok(git::status::GitDiffStat {
893 entries: entries.into(),
894 })
895 })
896 .boxed()
897 }
898
899 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
900 let executor = self.executor.clone();
901 let fs = self.fs.clone();
902 let checkpoints = self.checkpoints.clone();
903 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
904 async move {
905 executor.simulate_random_delay().await;
906 let oid = git::Oid::random(&mut *executor.rng().lock());
907 let entry = fs.entry(&repository_dir_path)?;
908 checkpoints.lock().insert(oid, entry);
909 Ok(GitRepositoryCheckpoint { commit_sha: oid })
910 }
911 .boxed()
912 }
913
914 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
915 let executor = self.executor.clone();
916 let fs = self.fs.clone();
917 let checkpoints = self.checkpoints.clone();
918 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
919 async move {
920 executor.simulate_random_delay().await;
921 let checkpoints = checkpoints.lock();
922 let entry = checkpoints
923 .get(&checkpoint.commit_sha)
924 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
925 fs.insert_entry(&repository_dir_path, entry.clone())?;
926 Ok(())
927 }
928 .boxed()
929 }
930
931 fn compare_checkpoints(
932 &self,
933 left: GitRepositoryCheckpoint,
934 right: GitRepositoryCheckpoint,
935 ) -> BoxFuture<'_, Result<bool>> {
936 let executor = self.executor.clone();
937 let checkpoints = self.checkpoints.clone();
938 async move {
939 executor.simulate_random_delay().await;
940 let checkpoints = checkpoints.lock();
941 let left = checkpoints
942 .get(&left.commit_sha)
943 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
944 let right = checkpoints
945 .get(&right.commit_sha)
946 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
947
948 Ok(left == right)
949 }
950 .boxed()
951 }
952
953 fn diff_checkpoints(
954 &self,
955 _base_checkpoint: GitRepositoryCheckpoint,
956 _target_checkpoint: GitRepositoryCheckpoint,
957 ) -> BoxFuture<'_, Result<String>> {
958 unimplemented!()
959 }
960
961 fn default_branch(
962 &self,
963 include_remote_name: bool,
964 ) -> BoxFuture<'_, Result<Option<SharedString>>> {
965 async move {
966 Ok(Some(if include_remote_name {
967 "origin/main".into()
968 } else {
969 "main".into()
970 }))
971 }
972 .boxed()
973 }
974
975 fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
976 self.with_state_async(true, move |state| {
977 state.remotes.insert(name, url);
978 Ok(())
979 })
980 }
981
982 fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
983 self.with_state_async(true, move |state| {
984 state.remotes.remove(&name);
985 Ok(())
986 })
987 }
988
989 fn initial_graph_data(
990 &self,
991 _log_source: LogSource,
992 _log_order: LogOrder,
993 request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
994 ) -> BoxFuture<'_, Result<()>> {
995 let fs = self.fs.clone();
996 let dot_git_path = self.dot_git_path.clone();
997 async move {
998 let graph_commits =
999 fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
1000
1001 for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1002 request_tx.send(chunk.to_vec()).await.ok();
1003 }
1004 Ok(())
1005 }
1006 .boxed()
1007 }
1008
1009 fn commit_data_reader(&self) -> Result<CommitDataReader> {
1010 anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
1011 }
1012
1013 fn set_trusted(&self, trusted: bool) {
1014 self.is_trusted
1015 .store(trusted, std::sync::atomic::Ordering::Release);
1016 }
1017
1018 fn is_trusted(&self) -> bool {
1019 self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1020 }
1021}