fake_git_repo.rs

  1use crate::FakeFs;
  2use anyhow::{Context as _, Result, anyhow};
  3use collections::{HashMap, HashSet};
  4use futures::future::{self, BoxFuture};
  5use git::{
  6    blame::Blame,
  7    repository::{
  8        AskPassDelegate, Branch, CommitDetails, CommitOptions, GitRepository,
  9        GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
 10    },
 11    status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
 12};
 13use gpui::{AsyncApp, BackgroundExecutor};
 14use ignore::gitignore::GitignoreBuilder;
 15use rope::Rope;
 16use smol::future::FutureExt as _;
 17use std::{path::PathBuf, sync::Arc};
 18
 19#[derive(Clone)]
 20pub struct FakeGitRepository {
 21    pub(crate) fs: Arc<FakeFs>,
 22    pub(crate) executor: BackgroundExecutor,
 23    pub(crate) dot_git_path: PathBuf,
 24    pub(crate) repository_dir_path: PathBuf,
 25    pub(crate) common_dir_path: PathBuf,
 26}
 27
 28#[derive(Debug, Clone)]
 29pub struct FakeGitRepositoryState {
 30    pub event_emitter: smol::channel::Sender<PathBuf>,
 31    pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
 32    pub head_contents: HashMap<RepoPath, String>,
 33    pub index_contents: HashMap<RepoPath, String>,
 34    pub blames: HashMap<RepoPath, Blame>,
 35    pub current_branch_name: Option<String>,
 36    pub branches: HashSet<String>,
 37    pub merge_head_shas: Vec<String>,
 38    pub simulated_index_write_error_message: Option<String>,
 39}
 40
 41impl FakeGitRepositoryState {
 42    pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
 43        FakeGitRepositoryState {
 44            event_emitter,
 45            head_contents: Default::default(),
 46            index_contents: Default::default(),
 47            unmerged_paths: Default::default(),
 48            blames: Default::default(),
 49            current_branch_name: Default::default(),
 50            branches: Default::default(),
 51            merge_head_shas: Default::default(),
 52            simulated_index_write_error_message: Default::default(),
 53        }
 54    }
 55}
 56
 57impl FakeGitRepository {
 58    fn with_state<F, T>(&self, write: bool, f: F) -> Result<T>
 59    where
 60        F: FnOnce(&mut FakeGitRepositoryState) -> T,
 61    {
 62        self.fs.with_git_state(&self.dot_git_path, write, f)
 63    }
 64
 65    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
 66    where
 67        F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
 68        T: Send,
 69    {
 70        let fs = self.fs.clone();
 71        let executor = self.executor.clone();
 72        let dot_git_path = self.dot_git_path.clone();
 73        async move {
 74            executor.simulate_random_delay().await;
 75            fs.with_git_state(&dot_git_path, write, f)?
 76        }
 77        .boxed()
 78    }
 79}
 80
 81impl GitRepository for FakeGitRepository {
 82    fn reload_index(&self) {}
 83
 84    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
 85        async {
 86            self.with_state_async(false, move |state| {
 87                state
 88                    .index_contents
 89                    .get(path.as_ref())
 90                    .ok_or_else(|| anyhow!("not present in index"))
 91                    .cloned()
 92            })
 93            .await
 94            .ok()
 95        }
 96        .boxed()
 97    }
 98
 99    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
100        async {
101            self.with_state_async(false, move |state| {
102                state
103                    .head_contents
104                    .get(path.as_ref())
105                    .ok_or_else(|| anyhow!("not present in HEAD"))
106                    .cloned()
107            })
108            .await
109            .ok()
110        }
111        .boxed()
112    }
113
114    fn load_commit(
115        &self,
116        _commit: String,
117        _cx: AsyncApp,
118    ) -> BoxFuture<Result<git::repository::CommitDiff>> {
119        unimplemented!()
120    }
121
122    fn set_index_text(
123        &self,
124        path: RepoPath,
125        content: Option<String>,
126        _env: Arc<HashMap<String, String>>,
127    ) -> BoxFuture<anyhow::Result<()>> {
128        self.with_state_async(true, move |state| {
129            if let Some(message) = state.simulated_index_write_error_message.clone() {
130                return Err(anyhow!("{}", message));
131            } else if let Some(content) = content {
132                state.index_contents.insert(path, content);
133            } else {
134                state.index_contents.remove(&path);
135            }
136            Ok(())
137        })
138    }
139
140    fn remote_url(&self, _name: &str) -> Option<String> {
141        None
142    }
143
144    fn head_sha(&self) -> Option<String> {
145        None
146    }
147
148    fn merge_head_shas(&self) -> Vec<String> {
149        self.with_state(false, |state| state.merge_head_shas.clone())
150            .unwrap()
151    }
152
153    fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
154        async {
155            Ok(CommitDetails {
156                sha: commit.into(),
157                ..Default::default()
158            })
159        }
160        .boxed()
161    }
162
163    fn reset(
164        &self,
165        _commit: String,
166        _mode: ResetMode,
167        _env: Arc<HashMap<String, String>>,
168    ) -> BoxFuture<Result<()>> {
169        unimplemented!()
170    }
171
172    fn checkout_files(
173        &self,
174        _commit: String,
175        _paths: Vec<RepoPath>,
176        _env: Arc<HashMap<String, String>>,
177    ) -> BoxFuture<Result<()>> {
178        unimplemented!()
179    }
180
181    fn path(&self) -> PathBuf {
182        self.repository_dir_path.clone()
183    }
184
185    fn main_repository_path(&self) -> PathBuf {
186        self.common_dir_path.clone()
187    }
188
189    fn merge_message(&self) -> BoxFuture<Option<String>> {
190        async move { None }.boxed()
191    }
192
193    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
194        let workdir_path = self.dot_git_path.parent().unwrap();
195
196        // Load gitignores
197        let ignores = workdir_path
198            .ancestors()
199            .filter_map(|dir| {
200                let ignore_path = dir.join(".gitignore");
201                let content = self.fs.read_file_sync(ignore_path).ok()?;
202                let content = String::from_utf8(content).ok()?;
203                let mut builder = GitignoreBuilder::new(dir);
204                for line in content.lines() {
205                    builder.add_line(Some(dir.into()), line).ok()?;
206                }
207                builder.build().ok()
208            })
209            .collect::<Vec<_>>();
210
211        // Load working copy files.
212        let git_files: HashMap<RepoPath, (String, bool)> = self
213            .fs
214            .files()
215            .iter()
216            .filter_map(|path| {
217                // TODO better simulate git status output in the case of submodules and worktrees
218                let repo_path = path.strip_prefix(workdir_path).ok()?;
219                let mut is_ignored = repo_path.starts_with(".git");
220                for ignore in &ignores {
221                    match ignore.matched_path_or_any_parents(path, false) {
222                        ignore::Match::None => {}
223                        ignore::Match::Ignore(_) => is_ignored = true,
224                        ignore::Match::Whitelist(_) => break,
225                    }
226                }
227                let content = self
228                    .fs
229                    .read_file_sync(path)
230                    .ok()
231                    .map(|content| String::from_utf8(content).unwrap())?;
232                Some((repo_path.into(), (content, is_ignored)))
233            })
234            .collect();
235
236        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
237            let mut entries = Vec::new();
238            let paths = state
239                .head_contents
240                .keys()
241                .chain(state.index_contents.keys())
242                .chain(git_files.keys())
243                .collect::<HashSet<_>>();
244            for path in paths {
245                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
246                    continue;
247                }
248
249                let head = state.head_contents.get(path);
250                let index = state.index_contents.get(path);
251                let unmerged = state.unmerged_paths.get(path);
252                let fs = git_files.get(path);
253                let status = match (unmerged, head, index, fs) {
254                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
255                    (_, Some(head), Some(index), Some((fs, _))) => {
256                        FileStatus::Tracked(TrackedStatus {
257                            index_status: if head == index {
258                                StatusCode::Unmodified
259                            } else {
260                                StatusCode::Modified
261                            },
262                            worktree_status: if fs == index {
263                                StatusCode::Unmodified
264                            } else {
265                                StatusCode::Modified
266                            },
267                        })
268                    }
269                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
270                        index_status: if head == index {
271                            StatusCode::Unmodified
272                        } else {
273                            StatusCode::Modified
274                        },
275                        worktree_status: StatusCode::Deleted,
276                    }),
277                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
278                        index_status: StatusCode::Deleted,
279                        worktree_status: StatusCode::Added,
280                    }),
281                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
282                        index_status: StatusCode::Deleted,
283                        worktree_status: StatusCode::Deleted,
284                    }),
285                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
286                        index_status: StatusCode::Added,
287                        worktree_status: if fs == index {
288                            StatusCode::Unmodified
289                        } else {
290                            StatusCode::Modified
291                        },
292                    }),
293                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
294                        index_status: StatusCode::Added,
295                        worktree_status: StatusCode::Deleted,
296                    }),
297                    (_, None, None, Some((_, is_ignored))) => {
298                        if *is_ignored {
299                            continue;
300                        }
301                        FileStatus::Untracked
302                    }
303                    (_, None, None, None) => {
304                        unreachable!();
305                    }
306                };
307                if status
308                    != FileStatus::Tracked(TrackedStatus {
309                        index_status: StatusCode::Unmodified,
310                        worktree_status: StatusCode::Unmodified,
311                    })
312                {
313                    entries.push((path.clone(), status));
314                }
315            }
316            entries.sort_by(|a, b| a.0.cmp(&b.0));
317            anyhow::Ok(GitStatus {
318                entries: entries.into(),
319            })
320        });
321        async move { result? }.boxed()
322    }
323
324    fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
325        self.with_state_async(false, move |state| {
326            let current_branch = &state.current_branch_name;
327            Ok(state
328                .branches
329                .iter()
330                .map(|branch_name| Branch {
331                    is_head: Some(branch_name) == current_branch.as_ref(),
332                    name: branch_name.into(),
333                    most_recent_commit: None,
334                    upstream: None,
335                })
336                .collect())
337        })
338    }
339
340    fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
341        self.with_state_async(true, |state| {
342            state.current_branch_name = Some(name);
343            Ok(())
344        })
345    }
346
347    fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
348        self.with_state_async(true, move |state| {
349            state.branches.insert(name.to_owned());
350            Ok(())
351        })
352    }
353
354    fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
355        self.with_state_async(false, move |state| {
356            state
357                .blames
358                .get(&path)
359                .with_context(|| format!("failed to get blame for {:?}", path.0))
360                .cloned()
361        })
362    }
363
364    fn stage_paths(
365        &self,
366        _paths: Vec<RepoPath>,
367        _env: Arc<HashMap<String, String>>,
368    ) -> BoxFuture<Result<()>> {
369        unimplemented!()
370    }
371
372    fn unstage_paths(
373        &self,
374        _paths: Vec<RepoPath>,
375        _env: Arc<HashMap<String, String>>,
376    ) -> BoxFuture<Result<()>> {
377        unimplemented!()
378    }
379
380    fn commit(
381        &self,
382        _message: gpui::SharedString,
383        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
384        _options: CommitOptions,
385        _env: Arc<HashMap<String, String>>,
386    ) -> BoxFuture<Result<()>> {
387        unimplemented!()
388    }
389
390    fn push(
391        &self,
392        _branch: String,
393        _remote: String,
394        _options: Option<PushOptions>,
395        _askpass: AskPassDelegate,
396        _env: Arc<HashMap<String, String>>,
397        _cx: AsyncApp,
398    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
399        unimplemented!()
400    }
401
402    fn pull(
403        &self,
404        _branch: String,
405        _remote: String,
406        _askpass: AskPassDelegate,
407        _env: Arc<HashMap<String, String>>,
408        _cx: AsyncApp,
409    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
410        unimplemented!()
411    }
412
413    fn fetch(
414        &self,
415        _askpass: AskPassDelegate,
416        _env: Arc<HashMap<String, String>>,
417        _cx: AsyncApp,
418    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
419        unimplemented!()
420    }
421
422    fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
423        unimplemented!()
424    }
425
426    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
427        future::ready(Ok(Vec::new())).boxed()
428    }
429
430    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
431        unimplemented!()
432    }
433
434    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
435        unimplemented!()
436    }
437
438    fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
439        unimplemented!()
440    }
441
442    fn compare_checkpoints(
443        &self,
444        _left: GitRepositoryCheckpoint,
445        _right: GitRepositoryCheckpoint,
446    ) -> BoxFuture<Result<bool>> {
447        unimplemented!()
448    }
449
450    fn diff_checkpoints(
451        &self,
452        _base_checkpoint: GitRepositoryCheckpoint,
453        _target_checkpoint: GitRepositoryCheckpoint,
454    ) -> BoxFuture<Result<String>> {
455        unimplemented!()
456    }
457}