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}