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