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};
18use ignore::gitignore::GitignoreBuilder;
19use parking_lot::Mutex;
20use rope::Rope;
21use smol::future::FutureExt as _;
22use std::{path::PathBuf, sync::Arc};
23use text::LineEnding;
24use util::{paths::PathStyle, rel_path::RelPath};
25
26#[derive(Clone)]
27pub struct FakeGitRepository {
28 pub(crate) fs: Arc<FakeFs>,
29 pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
30 pub(crate) executor: BackgroundExecutor,
31 pub(crate) dot_git_path: PathBuf,
32 pub(crate) repository_dir_path: PathBuf,
33 pub(crate) common_dir_path: PathBuf,
34}
35
36#[derive(Debug, Clone)]
37pub struct FakeGitRepositoryState {
38 pub event_emitter: smol::channel::Sender<PathBuf>,
39 pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
40 pub head_contents: HashMap<RepoPath, String>,
41 pub index_contents: HashMap<RepoPath, String>,
42 // everything in commit contents is in oids
43 pub merge_base_contents: HashMap<RepoPath, Oid>,
44 pub oids: HashMap<Oid, String>,
45 pub blames: HashMap<RepoPath, Blame>,
46 pub current_branch_name: Option<String>,
47 pub branches: HashSet<String>,
48 /// List of remotes, keys are names and values are URLs
49 pub remotes: HashMap<String, String>,
50 pub simulated_index_write_error_message: Option<String>,
51 pub refs: HashMap<String, String>,
52}
53
54impl FakeGitRepositoryState {
55 pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
56 FakeGitRepositoryState {
57 event_emitter,
58 head_contents: Default::default(),
59 index_contents: Default::default(),
60 unmerged_paths: Default::default(),
61 blames: Default::default(),
62 current_branch_name: Default::default(),
63 branches: Default::default(),
64 simulated_index_write_error_message: Default::default(),
65 refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
66 merge_base_contents: Default::default(),
67 oids: Default::default(),
68 remotes: HashMap::default(),
69 }
70 }
71}
72
73impl FakeGitRepository {
74 fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
75 where
76 F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
77 T: Send,
78 {
79 let fs = self.fs.clone();
80 let executor = self.executor.clone();
81 let dot_git_path = self.dot_git_path.clone();
82 async move {
83 executor.simulate_random_delay().await;
84 fs.with_git_state(&dot_git_path, write, f)?
85 }
86 .boxed()
87 }
88}
89
90impl GitRepository for FakeGitRepository {
91 fn reload_index(&self) {}
92
93 fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
94 let fut = self.with_state_async(false, move |state| {
95 state
96 .index_contents
97 .get(&path)
98 .context("not present in index")
99 .cloned()
100 });
101 self.executor.spawn(async move { fut.await.ok() }).boxed()
102 }
103
104 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
105 let fut = self.with_state_async(false, move |state| {
106 state
107 .head_contents
108 .get(&path)
109 .context("not present in HEAD")
110 .cloned()
111 });
112 self.executor.spawn(async move { fut.await.ok() }).boxed()
113 }
114
115 fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
116 self.with_state_async(false, move |state| {
117 state.oids.get(&oid).cloned().context("oid does not exist")
118 })
119 .boxed()
120 }
121
122 fn load_commit(
123 &self,
124 _commit: String,
125 _cx: AsyncApp,
126 ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
127 unimplemented!()
128 }
129
130 fn set_index_text(
131 &self,
132 path: RepoPath,
133 content: Option<String>,
134 _env: Arc<HashMap<String, String>>,
135 _is_executable: bool,
136 ) -> BoxFuture<'_, anyhow::Result<()>> {
137 self.with_state_async(true, move |state| {
138 if let Some(message) = &state.simulated_index_write_error_message {
139 anyhow::bail!("{message}");
140 } else if let Some(content) = content {
141 state.index_contents.insert(path, content);
142 } else {
143 state.index_contents.remove(&path);
144 }
145 Ok(())
146 })
147 }
148
149 fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
150 let name = name.to_string();
151 let fut = self.with_state_async(false, move |state| {
152 state
153 .remotes
154 .get(&name)
155 .context("remote not found")
156 .cloned()
157 });
158 async move { fut.await.ok() }.boxed()
159 }
160
161 fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
162 let mut entries = HashMap::default();
163 self.with_state_async(false, |state| {
164 for (path, content) in &state.head_contents {
165 let status = if let Some((oid, original)) = state
166 .merge_base_contents
167 .get(path)
168 .map(|oid| (oid, &state.oids[oid]))
169 {
170 if original == content {
171 continue;
172 }
173 TreeDiffStatus::Modified { old: *oid }
174 } else {
175 TreeDiffStatus::Added
176 };
177 entries.insert(path.clone(), status);
178 }
179 for (path, oid) in &state.merge_base_contents {
180 if !entries.contains_key(path) {
181 entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
182 }
183 }
184 Ok(TreeDiff { entries })
185 })
186 .boxed()
187 }
188
189 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
190 self.with_state_async(false, |state| {
191 Ok(revs
192 .into_iter()
193 .map(|rev| state.refs.get(&rev).cloned())
194 .collect())
195 })
196 }
197
198 fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
199 async {
200 Ok(CommitDetails {
201 sha: commit.into(),
202 message: "initial 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(
455 &self,
456 path: RepoPath,
457 _content: Rope,
458 _line_ending: LineEnding,
459 ) -> BoxFuture<'_, Result<git::blame::Blame>> {
460 self.with_state_async(false, move |state| {
461 state
462 .blames
463 .get(&path)
464 .with_context(|| format!("failed to get blame for {:?}", path))
465 .cloned()
466 })
467 }
468
469 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
470 self.file_history_paginated(path, 0, None)
471 }
472
473 fn file_history_paginated(
474 &self,
475 path: RepoPath,
476 _skip: usize,
477 _limit: Option<usize>,
478 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
479 async move {
480 Ok(git::repository::FileHistory {
481 entries: Vec::new(),
482 path,
483 })
484 }
485 .boxed()
486 }
487
488 fn stage_paths(
489 &self,
490 paths: Vec<RepoPath>,
491 _env: Arc<HashMap<String, String>>,
492 ) -> BoxFuture<'_, Result<()>> {
493 Box::pin(async move {
494 let contents = paths
495 .into_iter()
496 .map(|path| {
497 let abs_path = self
498 .dot_git_path
499 .parent()
500 .unwrap()
501 .join(&path.as_std_path());
502 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
503 })
504 .collect::<Vec<_>>();
505 let contents = join_all(contents).await;
506 self.with_state_async(true, move |state| {
507 for (path, content) in contents {
508 if let Some(content) = content {
509 state.index_contents.insert(path, content);
510 } else {
511 state.index_contents.remove(&path);
512 }
513 }
514 Ok(())
515 })
516 .await
517 })
518 }
519
520 fn unstage_paths(
521 &self,
522 paths: Vec<RepoPath>,
523 _env: Arc<HashMap<String, String>>,
524 ) -> BoxFuture<'_, Result<()>> {
525 self.with_state_async(true, move |state| {
526 for path in paths {
527 match state.head_contents.get(&path) {
528 Some(content) => state.index_contents.insert(path, content.clone()),
529 None => state.index_contents.remove(&path),
530 };
531 }
532 Ok(())
533 })
534 }
535
536 fn stash_paths(
537 &self,
538 _paths: Vec<RepoPath>,
539 _env: Arc<HashMap<String, String>>,
540 ) -> BoxFuture<'_, Result<()>> {
541 unimplemented!()
542 }
543
544 fn stash_pop(
545 &self,
546 _index: Option<usize>,
547 _env: Arc<HashMap<String, String>>,
548 ) -> BoxFuture<'_, Result<()>> {
549 unimplemented!()
550 }
551
552 fn stash_apply(
553 &self,
554 _index: Option<usize>,
555 _env: Arc<HashMap<String, String>>,
556 ) -> BoxFuture<'_, Result<()>> {
557 unimplemented!()
558 }
559
560 fn stash_drop(
561 &self,
562 _index: Option<usize>,
563 _env: Arc<HashMap<String, String>>,
564 ) -> BoxFuture<'_, Result<()>> {
565 unimplemented!()
566 }
567
568 fn commit(
569 &self,
570 _message: gpui::SharedString,
571 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
572 _options: CommitOptions,
573 _askpass: AskPassDelegate,
574 _env: Arc<HashMap<String, String>>,
575 ) -> BoxFuture<'_, Result<()>> {
576 async { Ok(()) }.boxed()
577 }
578
579 fn run_hook(
580 &self,
581 _hook: RunHook,
582 _env: Arc<HashMap<String, String>>,
583 ) -> BoxFuture<'_, Result<()>> {
584 async { Ok(()) }.boxed()
585 }
586
587 fn push(
588 &self,
589 _branch: String,
590 _remote_branch: String,
591 _remote: String,
592 _options: Option<PushOptions>,
593 _askpass: AskPassDelegate,
594 _env: Arc<HashMap<String, String>>,
595 _cx: AsyncApp,
596 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
597 unimplemented!()
598 }
599
600 fn pull(
601 &self,
602 _branch: Option<String>,
603 _remote: String,
604 _rebase: bool,
605 _askpass: AskPassDelegate,
606 _env: Arc<HashMap<String, String>>,
607 _cx: AsyncApp,
608 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
609 unimplemented!()
610 }
611
612 fn fetch(
613 &self,
614 _fetch_options: FetchOptions,
615 _askpass: AskPassDelegate,
616 _env: Arc<HashMap<String, String>>,
617 _cx: AsyncApp,
618 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
619 unimplemented!()
620 }
621
622 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
623 self.with_state_async(false, move |state| {
624 let remotes = state
625 .remotes
626 .keys()
627 .map(|r| Remote {
628 name: r.clone().into(),
629 })
630 .collect::<Vec<_>>();
631 Ok(remotes)
632 })
633 }
634
635 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
636 unimplemented!()
637 }
638
639 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
640 unimplemented!()
641 }
642
643 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
644 future::ready(Ok(Vec::new())).boxed()
645 }
646
647 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
648 unimplemented!()
649 }
650
651 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
652 let executor = self.executor.clone();
653 let fs = self.fs.clone();
654 let checkpoints = self.checkpoints.clone();
655 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
656 async move {
657 executor.simulate_random_delay().await;
658 let oid = git::Oid::random(&mut *executor.rng().lock());
659 let entry = fs.entry(&repository_dir_path)?;
660 checkpoints.lock().insert(oid, entry);
661 Ok(GitRepositoryCheckpoint { commit_sha: oid })
662 }
663 .boxed()
664 }
665
666 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
667 let executor = self.executor.clone();
668 let fs = self.fs.clone();
669 let checkpoints = self.checkpoints.clone();
670 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
671 async move {
672 executor.simulate_random_delay().await;
673 let checkpoints = checkpoints.lock();
674 let entry = checkpoints
675 .get(&checkpoint.commit_sha)
676 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
677 fs.insert_entry(&repository_dir_path, entry.clone())?;
678 Ok(())
679 }
680 .boxed()
681 }
682
683 fn compare_checkpoints(
684 &self,
685 left: GitRepositoryCheckpoint,
686 right: GitRepositoryCheckpoint,
687 ) -> BoxFuture<'_, Result<bool>> {
688 let executor = self.executor.clone();
689 let checkpoints = self.checkpoints.clone();
690 async move {
691 executor.simulate_random_delay().await;
692 let checkpoints = checkpoints.lock();
693 let left = checkpoints
694 .get(&left.commit_sha)
695 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
696 let right = checkpoints
697 .get(&right.commit_sha)
698 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
699
700 Ok(left == right)
701 }
702 .boxed()
703 }
704
705 fn diff_checkpoints(
706 &self,
707 _base_checkpoint: GitRepositoryCheckpoint,
708 _target_checkpoint: GitRepositoryCheckpoint,
709 ) -> BoxFuture<'_, Result<String>> {
710 unimplemented!()
711 }
712
713 fn default_branch(
714 &self,
715 include_remote_name: bool,
716 ) -> BoxFuture<'_, Result<Option<SharedString>>> {
717 async move {
718 Ok(Some(if include_remote_name {
719 "origin/main".into()
720 } else {
721 "main".into()
722 }))
723 }
724 .boxed()
725 }
726
727 fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
728 self.with_state_async(true, move |state| {
729 state.remotes.insert(name, url);
730 Ok(())
731 })
732 }
733
734 fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
735 self.with_state_async(true, move |state| {
736 state.remotes.remove(&name);
737 Ok(())
738 })
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use crate::{FakeFs, Fs};
745 use gpui::BackgroundExecutor;
746 use serde_json::json;
747 use std::path::Path;
748 use util::path;
749
750 #[gpui::test]
751 async fn test_checkpoints(executor: BackgroundExecutor) {
752 let fs = FakeFs::new(executor);
753 fs.insert_tree(
754 path!("/"),
755 json!({
756 "bar": {
757 "baz": "qux"
758 },
759 "foo": {
760 ".git": {},
761 "a": "lorem",
762 "b": "ipsum",
763 },
764 }),
765 )
766 .await;
767 fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
768 .unwrap();
769 let repository = fs
770 .open_repo(Path::new("/foo/.git"), Some("git".as_ref()))
771 .unwrap();
772
773 let checkpoint_1 = repository.checkpoint().await.unwrap();
774 fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
775 fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
776 let checkpoint_2 = repository.checkpoint().await.unwrap();
777 let checkpoint_3 = repository.checkpoint().await.unwrap();
778
779 assert!(
780 repository
781 .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
782 .await
783 .unwrap()
784 );
785 assert!(
786 !repository
787 .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
788 .await
789 .unwrap()
790 );
791
792 repository.restore_checkpoint(checkpoint_1).await.unwrap();
793 assert_eq!(
794 fs.files_with_contents(Path::new("")),
795 [
796 (Path::new(path!("/bar/baz")).into(), b"qux".into()),
797 (Path::new(path!("/foo/a")).into(), b"lorem".into()),
798 (Path::new(path!("/foo/b")).into(), b"ipsum".into())
799 ]
800 );
801 }
802}