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