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