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