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