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| {
385 let ref_name = if branch_name.starts_with("refs/") {
386 branch_name.into()
387 } else {
388 format!("refs/heads/{branch_name}").into()
389 };
390 Branch {
391 is_head: Some(branch_name) == current_branch.as_ref(),
392 ref_name,
393 most_recent_commit: None,
394 upstream: None,
395 }
396 })
397 .collect())
398 })
399 }
400
401 fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
402 unimplemented!()
403 }
404
405 fn create_worktree(
406 &self,
407 _: String,
408 _: PathBuf,
409 _: Option<String>,
410 ) -> BoxFuture<'_, Result<()>> {
411 unimplemented!()
412 }
413
414 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
415 self.with_state_async(true, |state| {
416 state.current_branch_name = Some(name);
417 Ok(())
418 })
419 }
420
421 fn create_branch(
422 &self,
423 name: String,
424 _base_branch: Option<String>,
425 ) -> BoxFuture<'_, Result<()>> {
426 self.with_state_async(true, move |state| {
427 state.branches.insert(name);
428 Ok(())
429 })
430 }
431
432 fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
433 self.with_state_async(true, move |state| {
434 if !state.branches.remove(&branch) {
435 bail!("no such branch: {branch}");
436 }
437 state.branches.insert(new_name.clone());
438 if state.current_branch_name == Some(branch) {
439 state.current_branch_name = Some(new_name);
440 }
441 Ok(())
442 })
443 }
444
445 fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
446 self.with_state_async(true, move |state| {
447 if !state.branches.remove(&name) {
448 bail!("no such branch: {name}");
449 }
450 Ok(())
451 })
452 }
453
454 fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
455 self.with_state_async(false, move |state| {
456 state
457 .blames
458 .get(&path)
459 .with_context(|| format!("failed to get blame for {:?}", path))
460 .cloned()
461 })
462 }
463
464 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
465 self.file_history_paginated(path, 0, None)
466 }
467
468 fn file_history_paginated(
469 &self,
470 path: RepoPath,
471 _skip: usize,
472 _limit: Option<usize>,
473 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
474 async move {
475 Ok(git::repository::FileHistory {
476 entries: Vec::new(),
477 path,
478 })
479 }
480 .boxed()
481 }
482
483 fn stage_paths(
484 &self,
485 paths: Vec<RepoPath>,
486 _env: Arc<HashMap<String, String>>,
487 ) -> BoxFuture<'_, Result<()>> {
488 Box::pin(async move {
489 let contents = paths
490 .into_iter()
491 .map(|path| {
492 let abs_path = self
493 .dot_git_path
494 .parent()
495 .unwrap()
496 .join(&path.as_std_path());
497 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
498 })
499 .collect::<Vec<_>>();
500 let contents = join_all(contents).await;
501 self.with_state_async(true, move |state| {
502 for (path, content) in contents {
503 if let Some(content) = content {
504 state.index_contents.insert(path, content);
505 } else {
506 state.index_contents.remove(&path);
507 }
508 }
509 Ok(())
510 })
511 .await
512 })
513 }
514
515 fn unstage_paths(
516 &self,
517 paths: Vec<RepoPath>,
518 _env: Arc<HashMap<String, String>>,
519 ) -> BoxFuture<'_, Result<()>> {
520 self.with_state_async(true, move |state| {
521 for path in paths {
522 match state.head_contents.get(&path) {
523 Some(content) => state.index_contents.insert(path, content.clone()),
524 None => state.index_contents.remove(&path),
525 };
526 }
527 Ok(())
528 })
529 }
530
531 fn stash_paths(
532 &self,
533 _paths: Vec<RepoPath>,
534 _env: Arc<HashMap<String, String>>,
535 ) -> BoxFuture<'_, Result<()>> {
536 unimplemented!()
537 }
538
539 fn stash_pop(
540 &self,
541 _index: Option<usize>,
542 _env: Arc<HashMap<String, String>>,
543 ) -> BoxFuture<'_, Result<()>> {
544 unimplemented!()
545 }
546
547 fn stash_apply(
548 &self,
549 _index: Option<usize>,
550 _env: Arc<HashMap<String, String>>,
551 ) -> BoxFuture<'_, Result<()>> {
552 unimplemented!()
553 }
554
555 fn stash_drop(
556 &self,
557 _index: Option<usize>,
558 _env: Arc<HashMap<String, String>>,
559 ) -> BoxFuture<'_, Result<()>> {
560 unimplemented!()
561 }
562
563 fn commit(
564 &self,
565 _message: gpui::SharedString,
566 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
567 _options: CommitOptions,
568 _askpass: AskPassDelegate,
569 _env: Arc<HashMap<String, String>>,
570 ) -> BoxFuture<'_, Result<()>> {
571 unimplemented!()
572 }
573
574 fn run_hook(
575 &self,
576 _hook: RunHook,
577 _env: Arc<HashMap<String, String>>,
578 ) -> BoxFuture<'_, Result<()>> {
579 unimplemented!()
580 }
581
582 fn push(
583 &self,
584 _branch: String,
585 _remote: String,
586 _options: Option<PushOptions>,
587 _askpass: AskPassDelegate,
588 _env: Arc<HashMap<String, String>>,
589 _cx: AsyncApp,
590 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
591 unimplemented!()
592 }
593
594 fn pull(
595 &self,
596 _branch: Option<String>,
597 _remote: String,
598 _rebase: bool,
599 _askpass: AskPassDelegate,
600 _env: Arc<HashMap<String, String>>,
601 _cx: AsyncApp,
602 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
603 unimplemented!()
604 }
605
606 fn fetch(
607 &self,
608 _fetch_options: FetchOptions,
609 _askpass: AskPassDelegate,
610 _env: Arc<HashMap<String, String>>,
611 _cx: AsyncApp,
612 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
613 unimplemented!()
614 }
615
616 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
617 self.with_state_async(false, move |state| {
618 let remotes = state
619 .remotes
620 .keys()
621 .map(|r| Remote {
622 name: r.clone().into(),
623 })
624 .collect::<Vec<_>>();
625 Ok(remotes)
626 })
627 }
628
629 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
630 unimplemented!()
631 }
632
633 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
634 unimplemented!()
635 }
636
637 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
638 future::ready(Ok(Vec::new())).boxed()
639 }
640
641 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
642 unimplemented!()
643 }
644
645 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
646 let executor = self.executor.clone();
647 let fs = self.fs.clone();
648 let checkpoints = self.checkpoints.clone();
649 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
650 async move {
651 executor.simulate_random_delay().await;
652 let oid = git::Oid::random(&mut executor.rng());
653 let entry = fs.entry(&repository_dir_path)?;
654 checkpoints.lock().insert(oid, entry);
655 Ok(GitRepositoryCheckpoint { commit_sha: oid })
656 }
657 .boxed()
658 }
659
660 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
661 let executor = self.executor.clone();
662 let fs = self.fs.clone();
663 let checkpoints = self.checkpoints.clone();
664 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
665 async move {
666 executor.simulate_random_delay().await;
667 let checkpoints = checkpoints.lock();
668 let entry = checkpoints
669 .get(&checkpoint.commit_sha)
670 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
671 fs.insert_entry(&repository_dir_path, entry.clone())?;
672 Ok(())
673 }
674 .boxed()
675 }
676
677 fn compare_checkpoints(
678 &self,
679 left: GitRepositoryCheckpoint,
680 right: GitRepositoryCheckpoint,
681 ) -> BoxFuture<'_, Result<bool>> {
682 let executor = self.executor.clone();
683 let checkpoints = self.checkpoints.clone();
684 async move {
685 executor.simulate_random_delay().await;
686 let checkpoints = checkpoints.lock();
687 let left = checkpoints
688 .get(&left.commit_sha)
689 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
690 let right = checkpoints
691 .get(&right.commit_sha)
692 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
693
694 Ok(left == right)
695 }
696 .boxed()
697 }
698
699 fn diff_checkpoints(
700 &self,
701 _base_checkpoint: GitRepositoryCheckpoint,
702 _target_checkpoint: GitRepositoryCheckpoint,
703 ) -> BoxFuture<'_, Result<String>> {
704 unimplemented!()
705 }
706
707 fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
708 async { Ok(Some("main".into())) }.boxed()
709 }
710
711 fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
712 self.with_state_async(true, move |state| {
713 state.remotes.insert(name, url);
714 Ok(())
715 })
716 }
717
718 fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
719 self.with_state_async(true, move |state| {
720 state.remotes.remove(&name);
721 Ok(())
722 })
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use crate::{FakeFs, Fs};
729 use gpui::BackgroundExecutor;
730 use serde_json::json;
731 use std::path::Path;
732 use util::path;
733
734 #[gpui::test]
735 async fn test_checkpoints(executor: BackgroundExecutor) {
736 let fs = FakeFs::new(executor);
737 fs.insert_tree(
738 path!("/"),
739 json!({
740 "bar": {
741 "baz": "qux"
742 },
743 "foo": {
744 ".git": {},
745 "a": "lorem",
746 "b": "ipsum",
747 },
748 }),
749 )
750 .await;
751 fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
752 .unwrap();
753 let repository = fs
754 .open_repo(Path::new("/foo/.git"), Some("git".as_ref()))
755 .unwrap();
756
757 let checkpoint_1 = repository.checkpoint().await.unwrap();
758 fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
759 fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
760 let checkpoint_2 = repository.checkpoint().await.unwrap();
761 let checkpoint_3 = repository.checkpoint().await.unwrap();
762
763 assert!(
764 repository
765 .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
766 .await
767 .unwrap()
768 );
769 assert!(
770 !repository
771 .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
772 .await
773 .unwrap()
774 );
775
776 repository.restore_checkpoint(checkpoint_1).await.unwrap();
777 assert_eq!(
778 fs.files_with_contents(Path::new("")),
779 [
780 (Path::new(path!("/bar/baz")).into(), b"qux".into()),
781 (Path::new(path!("/foo/a")).into(), b"lorem".into()),
782 (Path::new(path!("/foo/b")).into(), b"ipsum".into())
783 ]
784 );
785 }
786}