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};
 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}