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