fake_git_repo.rs

  1use crate::{FakeFs, FakeFsEntry, Fs};
  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};
 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}
 36
 37#[derive(Debug, Clone)]
 38pub struct FakeGitRepositoryState {
 39    pub event_emitter: smol::channel::Sender<PathBuf>,
 40    pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
 41    pub head_contents: HashMap<RepoPath, String>,
 42    pub index_contents: HashMap<RepoPath, String>,
 43    // everything in commit contents is in oids
 44    pub merge_base_contents: HashMap<RepoPath, Oid>,
 45    pub oids: HashMap<Oid, String>,
 46    pub blames: HashMap<RepoPath, Blame>,
 47    pub current_branch_name: Option<String>,
 48    pub branches: HashSet<String>,
 49    /// List of remotes, keys are names and values are URLs
 50    pub remotes: HashMap<String, String>,
 51    pub simulated_index_write_error_message: Option<String>,
 52    pub refs: HashMap<String, String>,
 53    pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
 54}
 55
 56impl FakeGitRepositoryState {
 57    pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
 58        FakeGitRepositoryState {
 59            event_emitter,
 60            head_contents: Default::default(),
 61            index_contents: Default::default(),
 62            unmerged_paths: Default::default(),
 63            blames: Default::default(),
 64            current_branch_name: Default::default(),
 65            branches: Default::default(),
 66            simulated_index_write_error_message: Default::default(),
 67            refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
 68            merge_base_contents: Default::default(),
 69            oids: Default::default(),
 70            remotes: HashMap::default(),
 71            graph_commits: Vec::new(),
 72        }
 73    }
 74}
 75
 76impl FakeGitRepository {
 77    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
 78    where
 79        F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
 80        T: Send,
 81    {
 82        let fs = self.fs.clone();
 83        let executor = self.executor.clone();
 84        let dot_git_path = self.dot_git_path.clone();
 85        async move {
 86            executor.simulate_random_delay().await;
 87            fs.with_git_state(&dot_git_path, write, f)?
 88        }
 89        .boxed()
 90    }
 91}
 92
 93impl GitRepository for FakeGitRepository {
 94    fn reload_index(&self) {}
 95
 96    fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
 97        let fut = self.with_state_async(false, move |state| {
 98            state
 99                .index_contents
100                .get(&path)
101                .context("not present in index")
102                .cloned()
103        });
104        self.executor.spawn(async move { fut.await.ok() }).boxed()
105    }
106
107    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
108        let fut = self.with_state_async(false, move |state| {
109            state
110                .head_contents
111                .get(&path)
112                .context("not present in HEAD")
113                .cloned()
114        });
115        self.executor.spawn(async move { fut.await.ok() }).boxed()
116    }
117
118    fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
119        self.with_state_async(false, move |state| {
120            state.oids.get(&oid).cloned().context("oid does not exist")
121        })
122        .boxed()
123    }
124
125    fn load_commit(
126        &self,
127        _commit: String,
128        _cx: AsyncApp,
129    ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
130        unimplemented!()
131    }
132
133    fn set_index_text(
134        &self,
135        path: RepoPath,
136        content: Option<String>,
137        _env: Arc<HashMap<String, String>>,
138        _is_executable: bool,
139    ) -> BoxFuture<'_, anyhow::Result<()>> {
140        self.with_state_async(true, move |state| {
141            if let Some(message) = &state.simulated_index_write_error_message {
142                anyhow::bail!("{message}");
143            } else if let Some(content) = content {
144                state.index_contents.insert(path, content);
145            } else {
146                state.index_contents.remove(&path);
147            }
148            Ok(())
149        })
150    }
151
152    fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
153        let name = name.to_string();
154        let fut = self.with_state_async(false, move |state| {
155            state
156                .remotes
157                .get(&name)
158                .context("remote not found")
159                .cloned()
160        });
161        async move { fut.await.ok() }.boxed()
162    }
163
164    fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
165        let mut entries = HashMap::default();
166        self.with_state_async(false, |state| {
167            for (path, content) in &state.head_contents {
168                let status = if let Some((oid, original)) = state
169                    .merge_base_contents
170                    .get(path)
171                    .map(|oid| (oid, &state.oids[oid]))
172                {
173                    if original == content {
174                        continue;
175                    }
176                    TreeDiffStatus::Modified { old: *oid }
177                } else {
178                    TreeDiffStatus::Added
179                };
180                entries.insert(path.clone(), status);
181            }
182            for (path, oid) in &state.merge_base_contents {
183                if !entries.contains_key(path) {
184                    entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
185                }
186            }
187            Ok(TreeDiff { entries })
188        })
189        .boxed()
190    }
191
192    fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
193        self.with_state_async(false, |state| {
194            Ok(revs
195                .into_iter()
196                .map(|rev| state.refs.get(&rev).cloned())
197                .collect())
198        })
199    }
200
201    fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
202        async {
203            Ok(CommitDetails {
204                sha: commit.into(),
205                message: "initial commit".into(),
206                ..Default::default()
207            })
208        }
209        .boxed()
210    }
211
212    fn reset(
213        &self,
214        _commit: String,
215        _mode: ResetMode,
216        _env: Arc<HashMap<String, String>>,
217    ) -> BoxFuture<'_, Result<()>> {
218        unimplemented!()
219    }
220
221    fn checkout_files(
222        &self,
223        _commit: String,
224        _paths: Vec<RepoPath>,
225        _env: Arc<HashMap<String, String>>,
226    ) -> BoxFuture<'_, Result<()>> {
227        unimplemented!()
228    }
229
230    fn path(&self) -> PathBuf {
231        self.repository_dir_path.clone()
232    }
233
234    fn main_repository_path(&self) -> PathBuf {
235        self.common_dir_path.clone()
236    }
237
238    fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
239        async move { None }.boxed()
240    }
241
242    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
243        let workdir_path = self.dot_git_path.parent().unwrap();
244
245        // Load gitignores
246        let ignores = workdir_path
247            .ancestors()
248            .filter_map(|dir| {
249                let ignore_path = dir.join(".gitignore");
250                let content = self.fs.read_file_sync(ignore_path).ok()?;
251                let content = String::from_utf8(content).ok()?;
252                let mut builder = GitignoreBuilder::new(dir);
253                for line in content.lines() {
254                    builder.add_line(Some(dir.into()), line).ok()?;
255                }
256                builder.build().ok()
257            })
258            .collect::<Vec<_>>();
259
260        // Load working copy files.
261        let git_files: HashMap<RepoPath, (String, bool)> = self
262            .fs
263            .files()
264            .iter()
265            .filter_map(|path| {
266                // TODO better simulate git status output in the case of submodules and worktrees
267                let repo_path = path.strip_prefix(workdir_path).ok()?;
268                let mut is_ignored = repo_path.starts_with(".git");
269                for ignore in &ignores {
270                    match ignore.matched_path_or_any_parents(path, false) {
271                        ignore::Match::None => {}
272                        ignore::Match::Ignore(_) => is_ignored = true,
273                        ignore::Match::Whitelist(_) => break,
274                    }
275                }
276                let content = self
277                    .fs
278                    .read_file_sync(path)
279                    .ok()
280                    .map(|content| String::from_utf8(content).unwrap())?;
281                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
282                Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
283            })
284            .collect();
285
286        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
287            let mut entries = Vec::new();
288            let paths = state
289                .head_contents
290                .keys()
291                .chain(state.index_contents.keys())
292                .chain(git_files.keys())
293                .collect::<HashSet<_>>();
294            for path in paths {
295                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
296                    continue;
297                }
298
299                let head = state.head_contents.get(path);
300                let index = state.index_contents.get(path);
301                let unmerged = state.unmerged_paths.get(path);
302                let fs = git_files.get(path);
303                let status = match (unmerged, head, index, fs) {
304                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
305                    (_, Some(head), Some(index), Some((fs, _))) => {
306                        FileStatus::Tracked(TrackedStatus {
307                            index_status: if head == index {
308                                StatusCode::Unmodified
309                            } else {
310                                StatusCode::Modified
311                            },
312                            worktree_status: if fs == index {
313                                StatusCode::Unmodified
314                            } else {
315                                StatusCode::Modified
316                            },
317                        })
318                    }
319                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
320                        index_status: if head == index {
321                            StatusCode::Unmodified
322                        } else {
323                            StatusCode::Modified
324                        },
325                        worktree_status: StatusCode::Deleted,
326                    }),
327                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
328                        index_status: StatusCode::Deleted,
329                        worktree_status: StatusCode::Added,
330                    }),
331                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
332                        index_status: StatusCode::Deleted,
333                        worktree_status: StatusCode::Deleted,
334                    }),
335                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
336                        index_status: StatusCode::Added,
337                        worktree_status: if fs == index {
338                            StatusCode::Unmodified
339                        } else {
340                            StatusCode::Modified
341                        },
342                    }),
343                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
344                        index_status: StatusCode::Added,
345                        worktree_status: StatusCode::Deleted,
346                    }),
347                    (_, None, None, Some((_, is_ignored))) => {
348                        if *is_ignored {
349                            continue;
350                        }
351                        FileStatus::Untracked
352                    }
353                    (_, None, None, None) => {
354                        unreachable!();
355                    }
356                };
357                if status
358                    != FileStatus::Tracked(TrackedStatus {
359                        index_status: StatusCode::Unmodified,
360                        worktree_status: StatusCode::Unmodified,
361                    })
362                {
363                    entries.push((path.clone(), status));
364                }
365            }
366            entries.sort_by(|a, b| a.0.cmp(&b.0));
367            anyhow::Ok(GitStatus {
368                entries: entries.into(),
369            })
370        });
371        Task::ready(match result {
372            Ok(result) => result,
373            Err(e) => Err(e),
374        })
375    }
376
377    fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
378        async { Ok(git::stash::GitStash::default()) }.boxed()
379    }
380
381    fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
382        self.with_state_async(false, move |state| {
383            let current_branch = &state.current_branch_name;
384            Ok(state
385                .branches
386                .iter()
387                .map(|branch_name| {
388                    let ref_name = if branch_name.starts_with("refs/") {
389                        branch_name.into()
390                    } else {
391                        format!("refs/heads/{branch_name}").into()
392                    };
393                    Branch {
394                        is_head: Some(branch_name) == current_branch.as_ref(),
395                        ref_name,
396                        most_recent_commit: None,
397                        upstream: None,
398                    }
399                })
400                .collect())
401        })
402    }
403
404    fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
405        unimplemented!()
406    }
407
408    fn create_worktree(
409        &self,
410        _: String,
411        _: PathBuf,
412        _: Option<String>,
413    ) -> BoxFuture<'_, Result<()>> {
414        unimplemented!()
415    }
416
417    fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
418        self.with_state_async(true, |state| {
419            state.current_branch_name = Some(name);
420            Ok(())
421        })
422    }
423
424    fn create_branch(
425        &self,
426        name: String,
427        _base_branch: Option<String>,
428    ) -> BoxFuture<'_, Result<()>> {
429        self.with_state_async(true, move |state| {
430            state.branches.insert(name);
431            Ok(())
432        })
433    }
434
435    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
436        self.with_state_async(true, move |state| {
437            if !state.branches.remove(&branch) {
438                bail!("no such branch: {branch}");
439            }
440            state.branches.insert(new_name.clone());
441            if state.current_branch_name == Some(branch) {
442                state.current_branch_name = Some(new_name);
443            }
444            Ok(())
445        })
446    }
447
448    fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
449        self.with_state_async(true, move |state| {
450            if !state.branches.remove(&name) {
451                bail!("no such branch: {name}");
452            }
453            Ok(())
454        })
455    }
456
457    fn blame(
458        &self,
459        path: RepoPath,
460        _content: Rope,
461        _line_ending: LineEnding,
462    ) -> BoxFuture<'_, Result<git::blame::Blame>> {
463        self.with_state_async(false, move |state| {
464            state
465                .blames
466                .get(&path)
467                .with_context(|| format!("failed to get blame for {:?}", path))
468                .cloned()
469        })
470    }
471
472    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
473        self.file_history_paginated(path, 0, None)
474    }
475
476    fn file_history_paginated(
477        &self,
478        path: RepoPath,
479        _skip: usize,
480        _limit: Option<usize>,
481    ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
482        async move {
483            Ok(git::repository::FileHistory {
484                entries: Vec::new(),
485                path,
486            })
487        }
488        .boxed()
489    }
490
491    fn stage_paths(
492        &self,
493        paths: Vec<RepoPath>,
494        _env: Arc<HashMap<String, String>>,
495    ) -> BoxFuture<'_, Result<()>> {
496        Box::pin(async move {
497            let contents = paths
498                .into_iter()
499                .map(|path| {
500                    let abs_path = self
501                        .dot_git_path
502                        .parent()
503                        .unwrap()
504                        .join(&path.as_std_path());
505                    Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
506                })
507                .collect::<Vec<_>>();
508            let contents = join_all(contents).await;
509            self.with_state_async(true, move |state| {
510                for (path, content) in contents {
511                    if let Some(content) = content {
512                        state.index_contents.insert(path, content);
513                    } else {
514                        state.index_contents.remove(&path);
515                    }
516                }
517                Ok(())
518            })
519            .await
520        })
521    }
522
523    fn unstage_paths(
524        &self,
525        paths: Vec<RepoPath>,
526        _env: Arc<HashMap<String, String>>,
527    ) -> BoxFuture<'_, Result<()>> {
528        self.with_state_async(true, move |state| {
529            for path in paths {
530                match state.head_contents.get(&path) {
531                    Some(content) => state.index_contents.insert(path, content.clone()),
532                    None => state.index_contents.remove(&path),
533                };
534            }
535            Ok(())
536        })
537    }
538
539    fn stash_paths(
540        &self,
541        _paths: Vec<RepoPath>,
542        _env: Arc<HashMap<String, String>>,
543    ) -> BoxFuture<'_, Result<()>> {
544        unimplemented!()
545    }
546
547    fn stash_pop(
548        &self,
549        _index: Option<usize>,
550        _env: Arc<HashMap<String, String>>,
551    ) -> BoxFuture<'_, Result<()>> {
552        unimplemented!()
553    }
554
555    fn stash_apply(
556        &self,
557        _index: Option<usize>,
558        _env: Arc<HashMap<String, String>>,
559    ) -> BoxFuture<'_, Result<()>> {
560        unimplemented!()
561    }
562
563    fn stash_drop(
564        &self,
565        _index: Option<usize>,
566        _env: Arc<HashMap<String, String>>,
567    ) -> BoxFuture<'_, Result<()>> {
568        unimplemented!()
569    }
570
571    fn commit(
572        &self,
573        _message: gpui::SharedString,
574        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
575        _options: CommitOptions,
576        _askpass: AskPassDelegate,
577        _env: Arc<HashMap<String, String>>,
578    ) -> BoxFuture<'_, Result<()>> {
579        async { Ok(()) }.boxed()
580    }
581
582    fn run_hook(
583        &self,
584        _hook: RunHook,
585        _env: Arc<HashMap<String, String>>,
586    ) -> BoxFuture<'_, Result<()>> {
587        async { Ok(()) }.boxed()
588    }
589
590    fn push(
591        &self,
592        _branch: String,
593        _remote_branch: String,
594        _remote: String,
595        _options: Option<PushOptions>,
596        _askpass: AskPassDelegate,
597        _env: Arc<HashMap<String, String>>,
598        _cx: AsyncApp,
599    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
600        unimplemented!()
601    }
602
603    fn pull(
604        &self,
605        _branch: Option<String>,
606        _remote: String,
607        _rebase: bool,
608        _askpass: AskPassDelegate,
609        _env: Arc<HashMap<String, String>>,
610        _cx: AsyncApp,
611    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
612        unimplemented!()
613    }
614
615    fn fetch(
616        &self,
617        _fetch_options: FetchOptions,
618        _askpass: AskPassDelegate,
619        _env: Arc<HashMap<String, String>>,
620        _cx: AsyncApp,
621    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
622        unimplemented!()
623    }
624
625    fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
626        self.with_state_async(false, move |state| {
627            let remotes = state
628                .remotes
629                .keys()
630                .map(|r| Remote {
631                    name: r.clone().into(),
632                })
633                .collect::<Vec<_>>();
634            Ok(remotes)
635        })
636    }
637
638    fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
639        unimplemented!()
640    }
641
642    fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
643        unimplemented!()
644    }
645
646    fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
647        future::ready(Ok(Vec::new())).boxed()
648    }
649
650    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
651        unimplemented!()
652    }
653
654    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
655        let executor = self.executor.clone();
656        let fs = self.fs.clone();
657        let checkpoints = self.checkpoints.clone();
658        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
659        async move {
660            executor.simulate_random_delay().await;
661            let oid = git::Oid::random(&mut *executor.rng().lock());
662            let entry = fs.entry(&repository_dir_path)?;
663            checkpoints.lock().insert(oid, entry);
664            Ok(GitRepositoryCheckpoint { commit_sha: oid })
665        }
666        .boxed()
667    }
668
669    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
670        let executor = self.executor.clone();
671        let fs = self.fs.clone();
672        let checkpoints = self.checkpoints.clone();
673        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
674        async move {
675            executor.simulate_random_delay().await;
676            let checkpoints = checkpoints.lock();
677            let entry = checkpoints
678                .get(&checkpoint.commit_sha)
679                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
680            fs.insert_entry(&repository_dir_path, entry.clone())?;
681            Ok(())
682        }
683        .boxed()
684    }
685
686    fn compare_checkpoints(
687        &self,
688        left: GitRepositoryCheckpoint,
689        right: GitRepositoryCheckpoint,
690    ) -> BoxFuture<'_, Result<bool>> {
691        let executor = self.executor.clone();
692        let checkpoints = self.checkpoints.clone();
693        async move {
694            executor.simulate_random_delay().await;
695            let checkpoints = checkpoints.lock();
696            let left = checkpoints
697                .get(&left.commit_sha)
698                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
699            let right = checkpoints
700                .get(&right.commit_sha)
701                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
702
703            Ok(left == right)
704        }
705        .boxed()
706    }
707
708    fn diff_checkpoints(
709        &self,
710        _base_checkpoint: GitRepositoryCheckpoint,
711        _target_checkpoint: GitRepositoryCheckpoint,
712    ) -> BoxFuture<'_, Result<String>> {
713        unimplemented!()
714    }
715
716    fn default_branch(
717        &self,
718        include_remote_name: bool,
719    ) -> BoxFuture<'_, Result<Option<SharedString>>> {
720        async move {
721            Ok(Some(if include_remote_name {
722                "origin/main".into()
723            } else {
724                "main".into()
725            }))
726        }
727        .boxed()
728    }
729
730    fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
731        self.with_state_async(true, move |state| {
732            state.remotes.insert(name, url);
733            Ok(())
734        })
735    }
736
737    fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
738        self.with_state_async(true, move |state| {
739            state.remotes.remove(&name);
740            Ok(())
741        })
742    }
743
744    fn initial_graph_data(
745        &self,
746        _log_source: LogSource,
747        _log_order: LogOrder,
748        request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
749    ) -> BoxFuture<'_, Result<()>> {
750        let fs = self.fs.clone();
751        let dot_git_path = self.dot_git_path.clone();
752        async move {
753            let graph_commits =
754                fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
755
756            for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
757                request_tx.send(chunk.to_vec()).await.ok();
758            }
759            Ok(())
760        }
761        .boxed()
762    }
763
764    fn commit_data_reader(&self) -> Result<CommitDataReader> {
765        anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
766    }
767}