fake_git_repo.rs

  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}