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                ..Default::default()
204            })
205        }
206        .boxed()
207    }
208
209    fn reset(
210        &self,
211        _commit: String,
212        _mode: ResetMode,
213        _env: Arc<HashMap<String, String>>,
214    ) -> BoxFuture<'_, Result<()>> {
215        unimplemented!()
216    }
217
218    fn checkout_files(
219        &self,
220        _commit: String,
221        _paths: Vec<RepoPath>,
222        _env: Arc<HashMap<String, String>>,
223    ) -> BoxFuture<'_, Result<()>> {
224        unimplemented!()
225    }
226
227    fn path(&self) -> PathBuf {
228        self.repository_dir_path.clone()
229    }
230
231    fn main_repository_path(&self) -> PathBuf {
232        self.common_dir_path.clone()
233    }
234
235    fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
236        async move { None }.boxed()
237    }
238
239    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
240        let workdir_path = self.dot_git_path.parent().unwrap();
241
242        // Load gitignores
243        let ignores = workdir_path
244            .ancestors()
245            .filter_map(|dir| {
246                let ignore_path = dir.join(".gitignore");
247                let content = self.fs.read_file_sync(ignore_path).ok()?;
248                let content = String::from_utf8(content).ok()?;
249                let mut builder = GitignoreBuilder::new(dir);
250                for line in content.lines() {
251                    builder.add_line(Some(dir.into()), line).ok()?;
252                }
253                builder.build().ok()
254            })
255            .collect::<Vec<_>>();
256
257        // Load working copy files.
258        let git_files: HashMap<RepoPath, (String, bool)> = self
259            .fs
260            .files()
261            .iter()
262            .filter_map(|path| {
263                // TODO better simulate git status output in the case of submodules and worktrees
264                let repo_path = path.strip_prefix(workdir_path).ok()?;
265                let mut is_ignored = repo_path.starts_with(".git");
266                for ignore in &ignores {
267                    match ignore.matched_path_or_any_parents(path, false) {
268                        ignore::Match::None => {}
269                        ignore::Match::Ignore(_) => is_ignored = true,
270                        ignore::Match::Whitelist(_) => break,
271                    }
272                }
273                let content = self
274                    .fs
275                    .read_file_sync(path)
276                    .ok()
277                    .map(|content| String::from_utf8(content).unwrap())?;
278                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
279                Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
280            })
281            .collect();
282
283        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
284            let mut entries = Vec::new();
285            let paths = state
286                .head_contents
287                .keys()
288                .chain(state.index_contents.keys())
289                .chain(git_files.keys())
290                .collect::<HashSet<_>>();
291            for path in paths {
292                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
293                    continue;
294                }
295
296                let head = state.head_contents.get(path);
297                let index = state.index_contents.get(path);
298                let unmerged = state.unmerged_paths.get(path);
299                let fs = git_files.get(path);
300                let status = match (unmerged, head, index, fs) {
301                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
302                    (_, Some(head), Some(index), Some((fs, _))) => {
303                        FileStatus::Tracked(TrackedStatus {
304                            index_status: if head == index {
305                                StatusCode::Unmodified
306                            } else {
307                                StatusCode::Modified
308                            },
309                            worktree_status: if fs == index {
310                                StatusCode::Unmodified
311                            } else {
312                                StatusCode::Modified
313                            },
314                        })
315                    }
316                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
317                        index_status: if head == index {
318                            StatusCode::Unmodified
319                        } else {
320                            StatusCode::Modified
321                        },
322                        worktree_status: StatusCode::Deleted,
323                    }),
324                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
325                        index_status: StatusCode::Deleted,
326                        worktree_status: StatusCode::Added,
327                    }),
328                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
329                        index_status: StatusCode::Deleted,
330                        worktree_status: StatusCode::Deleted,
331                    }),
332                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
333                        index_status: StatusCode::Added,
334                        worktree_status: if fs == index {
335                            StatusCode::Unmodified
336                        } else {
337                            StatusCode::Modified
338                        },
339                    }),
340                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
341                        index_status: StatusCode::Added,
342                        worktree_status: StatusCode::Deleted,
343                    }),
344                    (_, None, None, Some((_, is_ignored))) => {
345                        if *is_ignored {
346                            continue;
347                        }
348                        FileStatus::Untracked
349                    }
350                    (_, None, None, None) => {
351                        unreachable!();
352                    }
353                };
354                if status
355                    != FileStatus::Tracked(TrackedStatus {
356                        index_status: StatusCode::Unmodified,
357                        worktree_status: StatusCode::Unmodified,
358                    })
359                {
360                    entries.push((path.clone(), status));
361                }
362            }
363            entries.sort_by(|a, b| a.0.cmp(&b.0));
364            anyhow::Ok(GitStatus {
365                entries: entries.into(),
366            })
367        });
368        Task::ready(match result {
369            Ok(result) => result,
370            Err(e) => Err(e),
371        })
372    }
373
374    fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
375        async { Ok(git::stash::GitStash::default()) }.boxed()
376    }
377
378    fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
379        self.with_state_async(false, move |state| {
380            let current_branch = &state.current_branch_name;
381            Ok(state
382                .branches
383                .iter()
384                .map(|branch_name| Branch {
385                    is_head: Some(branch_name) == current_branch.as_ref(),
386                    ref_name: branch_name.into(),
387                    most_recent_commit: None,
388                    upstream: None,
389                })
390                .collect())
391        })
392    }
393
394    fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
395        unimplemented!()
396    }
397
398    fn create_worktree(
399        &self,
400        _: String,
401        _: PathBuf,
402        _: Option<String>,
403    ) -> BoxFuture<'_, Result<()>> {
404        unimplemented!()
405    }
406
407    fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
408        self.with_state_async(true, |state| {
409            state.current_branch_name = Some(name);
410            Ok(())
411        })
412    }
413
414    fn create_branch(
415        &self,
416        name: String,
417        _base_branch: Option<String>,
418    ) -> BoxFuture<'_, Result<()>> {
419        self.with_state_async(true, move |state| {
420            state.branches.insert(name);
421            Ok(())
422        })
423    }
424
425    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
426        self.with_state_async(true, move |state| {
427            if !state.branches.remove(&branch) {
428                bail!("no such branch: {branch}");
429            }
430            state.branches.insert(new_name.clone());
431            if state.current_branch_name == Some(branch) {
432                state.current_branch_name = Some(new_name);
433            }
434            Ok(())
435        })
436    }
437
438    fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
439        self.with_state_async(true, move |state| {
440            if !state.branches.remove(&name) {
441                bail!("no such branch: {name}");
442            }
443            Ok(())
444        })
445    }
446
447    fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
448        self.with_state_async(false, move |state| {
449            state
450                .blames
451                .get(&path)
452                .with_context(|| format!("failed to get blame for {:?}", path))
453                .cloned()
454        })
455    }
456
457    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
458        self.file_history_paginated(path, 0, None)
459    }
460
461    fn file_history_paginated(
462        &self,
463        path: RepoPath,
464        _skip: usize,
465        _limit: Option<usize>,
466    ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
467        async move {
468            Ok(git::repository::FileHistory {
469                entries: Vec::new(),
470                path,
471            })
472        }
473        .boxed()
474    }
475
476    fn stage_paths(
477        &self,
478        paths: Vec<RepoPath>,
479        _env: Arc<HashMap<String, String>>,
480    ) -> BoxFuture<'_, Result<()>> {
481        Box::pin(async move {
482            let contents = paths
483                .into_iter()
484                .map(|path| {
485                    let abs_path = self
486                        .dot_git_path
487                        .parent()
488                        .unwrap()
489                        .join(&path.as_std_path());
490                    Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
491                })
492                .collect::<Vec<_>>();
493            let contents = join_all(contents).await;
494            self.with_state_async(true, move |state| {
495                for (path, content) in contents {
496                    if let Some(content) = content {
497                        state.index_contents.insert(path, content);
498                    } else {
499                        state.index_contents.remove(&path);
500                    }
501                }
502                Ok(())
503            })
504            .await
505        })
506    }
507
508    fn unstage_paths(
509        &self,
510        paths: Vec<RepoPath>,
511        _env: Arc<HashMap<String, String>>,
512    ) -> BoxFuture<'_, Result<()>> {
513        self.with_state_async(true, move |state| {
514            for path in paths {
515                match state.head_contents.get(&path) {
516                    Some(content) => state.index_contents.insert(path, content.clone()),
517                    None => state.index_contents.remove(&path),
518                };
519            }
520            Ok(())
521        })
522    }
523
524    fn stash_paths(
525        &self,
526        _paths: Vec<RepoPath>,
527        _env: Arc<HashMap<String, String>>,
528    ) -> BoxFuture<'_, Result<()>> {
529        unimplemented!()
530    }
531
532    fn stash_pop(
533        &self,
534        _index: Option<usize>,
535        _env: Arc<HashMap<String, String>>,
536    ) -> BoxFuture<'_, Result<()>> {
537        unimplemented!()
538    }
539
540    fn stash_apply(
541        &self,
542        _index: Option<usize>,
543        _env: Arc<HashMap<String, String>>,
544    ) -> BoxFuture<'_, Result<()>> {
545        unimplemented!()
546    }
547
548    fn stash_drop(
549        &self,
550        _index: Option<usize>,
551        _env: Arc<HashMap<String, String>>,
552    ) -> BoxFuture<'_, Result<()>> {
553        unimplemented!()
554    }
555
556    fn commit(
557        &self,
558        _message: gpui::SharedString,
559        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
560        _options: CommitOptions,
561        _askpass: AskPassDelegate,
562        _env: Arc<HashMap<String, String>>,
563    ) -> BoxFuture<'_, Result<()>> {
564        unimplemented!()
565    }
566
567    fn run_hook(
568        &self,
569        _hook: RunHook,
570        _env: Arc<HashMap<String, String>>,
571    ) -> BoxFuture<'_, Result<()>> {
572        unimplemented!()
573    }
574
575    fn push(
576        &self,
577        _branch: String,
578        _remote: String,
579        _options: Option<PushOptions>,
580        _askpass: AskPassDelegate,
581        _env: Arc<HashMap<String, String>>,
582        _cx: AsyncApp,
583    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
584        unimplemented!()
585    }
586
587    fn pull(
588        &self,
589        _branch: Option<String>,
590        _remote: String,
591        _rebase: bool,
592        _askpass: AskPassDelegate,
593        _env: Arc<HashMap<String, String>>,
594        _cx: AsyncApp,
595    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
596        unimplemented!()
597    }
598
599    fn fetch(
600        &self,
601        _fetch_options: FetchOptions,
602        _askpass: AskPassDelegate,
603        _env: Arc<HashMap<String, String>>,
604        _cx: AsyncApp,
605    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
606        unimplemented!()
607    }
608
609    fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
610        self.with_state_async(false, move |state| {
611            let remotes = state
612                .remotes
613                .keys()
614                .map(|r| Remote {
615                    name: r.clone().into(),
616                })
617                .collect::<Vec<_>>();
618            Ok(remotes)
619        })
620    }
621
622    fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
623        unimplemented!()
624    }
625
626    fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
627        unimplemented!()
628    }
629
630    fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
631        future::ready(Ok(Vec::new())).boxed()
632    }
633
634    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
635        unimplemented!()
636    }
637
638    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
639        let executor = self.executor.clone();
640        let fs = self.fs.clone();
641        let checkpoints = self.checkpoints.clone();
642        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
643        async move {
644            executor.simulate_random_delay().await;
645            let oid = git::Oid::random(&mut executor.rng());
646            let entry = fs.entry(&repository_dir_path)?;
647            checkpoints.lock().insert(oid, entry);
648            Ok(GitRepositoryCheckpoint { commit_sha: oid })
649        }
650        .boxed()
651    }
652
653    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
654        let executor = self.executor.clone();
655        let fs = self.fs.clone();
656        let checkpoints = self.checkpoints.clone();
657        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
658        async move {
659            executor.simulate_random_delay().await;
660            let checkpoints = checkpoints.lock();
661            let entry = checkpoints
662                .get(&checkpoint.commit_sha)
663                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
664            fs.insert_entry(&repository_dir_path, entry.clone())?;
665            Ok(())
666        }
667        .boxed()
668    }
669
670    fn compare_checkpoints(
671        &self,
672        left: GitRepositoryCheckpoint,
673        right: GitRepositoryCheckpoint,
674    ) -> BoxFuture<'_, Result<bool>> {
675        let executor = self.executor.clone();
676        let checkpoints = self.checkpoints.clone();
677        async move {
678            executor.simulate_random_delay().await;
679            let checkpoints = checkpoints.lock();
680            let left = checkpoints
681                .get(&left.commit_sha)
682                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
683            let right = checkpoints
684                .get(&right.commit_sha)
685                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
686
687            Ok(left == right)
688        }
689        .boxed()
690    }
691
692    fn diff_checkpoints(
693        &self,
694        _base_checkpoint: GitRepositoryCheckpoint,
695        _target_checkpoint: GitRepositoryCheckpoint,
696    ) -> BoxFuture<'_, Result<String>> {
697        unimplemented!()
698    }
699
700    fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
701        async { Ok(Some("main".into())) }.boxed()
702    }
703
704    fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
705        self.with_state_async(true, move |state| {
706            state.remotes.insert(name, url);
707            Ok(())
708        })
709    }
710
711    fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
712        self.with_state_async(true, move |state| {
713            state.remotes.remove(&name);
714            Ok(())
715        })
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use crate::{FakeFs, Fs};
722    use gpui::BackgroundExecutor;
723    use serde_json::json;
724    use std::path::Path;
725    use util::path;
726
727    #[gpui::test]
728    async fn test_checkpoints(executor: BackgroundExecutor) {
729        let fs = FakeFs::new(executor);
730        fs.insert_tree(
731            path!("/"),
732            json!({
733                "bar": {
734                    "baz": "qux"
735                },
736                "foo": {
737                    ".git": {},
738                    "a": "lorem",
739                    "b": "ipsum",
740                },
741            }),
742        )
743        .await;
744        fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
745            .unwrap();
746        let repository = fs
747            .open_repo(Path::new("/foo/.git"), Some("git".as_ref()))
748            .unwrap();
749
750        let checkpoint_1 = repository.checkpoint().await.unwrap();
751        fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
752        fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
753        let checkpoint_2 = repository.checkpoint().await.unwrap();
754        let checkpoint_3 = repository.checkpoint().await.unwrap();
755
756        assert!(
757            repository
758                .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
759                .await
760                .unwrap()
761        );
762        assert!(
763            !repository
764                .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
765                .await
766                .unwrap()
767        );
768
769        repository.restore_checkpoint(checkpoint_1).await.unwrap();
770        assert_eq!(
771            fs.files_with_contents(Path::new("")),
772            [
773                (Path::new(path!("/bar/baz")).into(), b"qux".into()),
774                (Path::new(path!("/foo/a")).into(), b"lorem".into()),
775                (Path::new(path!("/foo/b")).into(), b"ipsum".into())
776            ]
777        );
778    }
779}