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