fake_git_repo.rs

  1use crate::FakeFs;
  2use anyhow::{anyhow, Context as _, Result};
  3use collections::{HashMap, HashSet};
  4use futures::future::{self, BoxFuture};
  5use git::{
  6    blame::Blame,
  7    repository::{
  8        AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
  9        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}
 25
 26#[derive(Debug, Clone)]
 27pub struct FakeGitRepositoryState {
 28    pub path: PathBuf,
 29    pub event_emitter: smol::channel::Sender<PathBuf>,
 30    pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
 31    pub head_contents: HashMap<RepoPath, String>,
 32    pub index_contents: HashMap<RepoPath, String>,
 33    pub blames: HashMap<RepoPath, Blame>,
 34    pub current_branch_name: Option<String>,
 35    pub branches: HashSet<String>,
 36    pub simulated_index_write_error_message: Option<String>,
 37}
 38
 39impl FakeGitRepositoryState {
 40    pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
 41        FakeGitRepositoryState {
 42            path,
 43            event_emitter,
 44            head_contents: Default::default(),
 45            index_contents: Default::default(),
 46            unmerged_paths: Default::default(),
 47            blames: Default::default(),
 48            current_branch_name: Default::default(),
 49            branches: Default::default(),
 50            simulated_index_write_error_message: Default::default(),
 51        }
 52    }
 53}
 54
 55impl FakeGitRepository {
 56    fn with_state<F, T>(&self, f: F) -> T
 57    where
 58        F: FnOnce(&mut FakeGitRepositoryState) -> T,
 59    {
 60        self.fs
 61            .with_git_state(&self.dot_git_path, false, f)
 62            .unwrap()
 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(
 85        &self,
 86        index: Option<GitIndex>,
 87        path: RepoPath,
 88    ) -> BoxFuture<Option<String>> {
 89        if index.is_some() {
 90            unimplemented!();
 91        }
 92
 93        async {
 94            self.with_state_async(false, move |state| {
 95                state
 96                    .index_contents
 97                    .get(path.as_ref())
 98                    .ok_or_else(|| anyhow!("not present in index"))
 99                    .cloned()
100            })
101            .await
102            .ok()
103        }
104        .boxed()
105    }
106
107    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
108        async {
109            self.with_state_async(false, move |state| {
110                state
111                    .head_contents
112                    .get(path.as_ref())
113                    .ok_or_else(|| anyhow!("not present in HEAD"))
114                    .cloned()
115            })
116            .await
117            .ok()
118        }
119        .boxed()
120    }
121
122    fn set_index_text(
123        &self,
124        path: RepoPath,
125        content: Option<String>,
126        _env: 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        vec![]
150    }
151
152    fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
153        unimplemented!()
154    }
155
156    fn reset(
157        &self,
158        _commit: String,
159        _mode: ResetMode,
160        _env: HashMap<String, String>,
161    ) -> BoxFuture<Result<()>> {
162        unimplemented!()
163    }
164
165    fn checkout_files(
166        &self,
167        _commit: String,
168        _paths: Vec<RepoPath>,
169        _env: HashMap<String, String>,
170    ) -> BoxFuture<Result<()>> {
171        unimplemented!()
172    }
173
174    fn path(&self) -> PathBuf {
175        self.with_state(|state| state.path.clone())
176    }
177
178    fn main_repository_path(&self) -> PathBuf {
179        self.path()
180    }
181
182    fn status(
183        &self,
184        index: Option<GitIndex>,
185        path_prefixes: &[RepoPath],
186    ) -> BoxFuture<'static, Result<GitStatus>> {
187        if index.is_some() {
188            unimplemented!();
189        }
190
191        let status = self.status_blocking(path_prefixes);
192        async move { status }.boxed()
193    }
194
195    fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
196        let workdir_path = self.dot_git_path.parent().unwrap();
197
198        // Load gitignores
199        let ignores = workdir_path
200            .ancestors()
201            .filter_map(|dir| {
202                let ignore_path = dir.join(".gitignore");
203                let content = self.fs.read_file_sync(ignore_path).ok()?;
204                let content = String::from_utf8(content).ok()?;
205                let mut builder = GitignoreBuilder::new(dir);
206                for line in content.lines() {
207                    builder.add_line(Some(dir.into()), line).ok()?;
208                }
209                builder.build().ok()
210            })
211            .collect::<Vec<_>>();
212
213        // Load working copy files.
214        let git_files: HashMap<RepoPath, (String, bool)> = self
215            .fs
216            .files()
217            .iter()
218            .filter_map(|path| {
219                let repo_path = path.strip_prefix(workdir_path).ok()?;
220                let mut is_ignored = false;
221                for ignore in &ignores {
222                    match ignore.matched_path_or_any_parents(path, false) {
223                        ignore::Match::None => {}
224                        ignore::Match::Ignore(_) => is_ignored = true,
225                        ignore::Match::Whitelist(_) => break,
226                    }
227                }
228                let content = self
229                    .fs
230                    .read_file_sync(path)
231                    .ok()
232                    .map(|content| String::from_utf8(content).unwrap())?;
233                Some((repo_path.into(), (content, is_ignored)))
234            })
235            .collect();
236
237        self.fs.with_git_state(&self.dot_git_path, false, |state| {
238            let mut entries = Vec::new();
239            let paths = state
240                .head_contents
241                .keys()
242                .chain(state.index_contents.keys())
243                .chain(git_files.keys())
244                .collect::<HashSet<_>>();
245            for path in paths {
246                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
247                    continue;
248                }
249
250                let head = state.head_contents.get(path);
251                let index = state.index_contents.get(path);
252                let unmerged = state.unmerged_paths.get(path);
253                let fs = git_files.get(path);
254                let status = match (unmerged, head, index, fs) {
255                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
256                    (_, Some(head), Some(index), Some((fs, _))) => {
257                        FileStatus::Tracked(TrackedStatus {
258                            index_status: if head == index {
259                                StatusCode::Unmodified
260                            } else {
261                                StatusCode::Modified
262                            },
263                            worktree_status: if fs == index {
264                                StatusCode::Unmodified
265                            } else {
266                                StatusCode::Modified
267                            },
268                        })
269                    }
270                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
271                        index_status: if head == index {
272                            StatusCode::Unmodified
273                        } else {
274                            StatusCode::Modified
275                        },
276                        worktree_status: StatusCode::Deleted,
277                    }),
278                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
279                        index_status: StatusCode::Deleted,
280                        worktree_status: StatusCode::Added,
281                    }),
282                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
283                        index_status: StatusCode::Deleted,
284                        worktree_status: StatusCode::Deleted,
285                    }),
286                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
287                        index_status: StatusCode::Added,
288                        worktree_status: if fs == index {
289                            StatusCode::Unmodified
290                        } else {
291                            StatusCode::Modified
292                        },
293                    }),
294                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
295                        index_status: StatusCode::Added,
296                        worktree_status: StatusCode::Deleted,
297                    }),
298                    (_, None, None, Some((_, is_ignored))) => {
299                        if *is_ignored {
300                            continue;
301                        }
302                        FileStatus::Untracked
303                    }
304                    (_, None, None, None) => {
305                        unreachable!();
306                    }
307                };
308                if status
309                    != FileStatus::Tracked(TrackedStatus {
310                        index_status: StatusCode::Unmodified,
311                        worktree_status: StatusCode::Unmodified,
312                    })
313                {
314                    entries.push((path.clone(), status));
315                }
316            }
317            entries.sort_by(|a, b| a.0.cmp(&b.0));
318            Ok(GitStatus {
319                entries: entries.into(),
320            })
321        })?
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: HashMap<String, String>,
368    ) -> BoxFuture<Result<()>> {
369        unimplemented!()
370    }
371
372    fn unstage_paths(
373        &self,
374        _paths: Vec<RepoPath>,
375        _env: 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        _env: HashMap<String, String>,
385    ) -> BoxFuture<Result<()>> {
386        unimplemented!()
387    }
388
389    fn push(
390        &self,
391        _branch: String,
392        _remote: String,
393        _options: Option<PushOptions>,
394        _askpass: AskPassSession,
395        _env: HashMap<String, String>,
396        _cx: AsyncApp,
397    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
398        unimplemented!()
399    }
400
401    fn pull(
402        &self,
403        _branch: String,
404        _remote: String,
405        _askpass: AskPassSession,
406        _env: HashMap<String, String>,
407        _cx: AsyncApp,
408    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
409        unimplemented!()
410    }
411
412    fn fetch(
413        &self,
414        _askpass: AskPassSession,
415        _env: HashMap<String, String>,
416        _cx: AsyncApp,
417    ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
418        unimplemented!()
419    }
420
421    fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
422        unimplemented!()
423    }
424
425    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
426        future::ready(Ok(Vec::new())).boxed()
427    }
428
429    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
430        unimplemented!()
431    }
432
433    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
434        unimplemented!()
435    }
436
437    fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
438        unimplemented!()
439    }
440
441    fn compare_checkpoints(
442        &self,
443        _left: GitRepositoryCheckpoint,
444        _right: GitRepositoryCheckpoint,
445    ) -> BoxFuture<Result<bool>> {
446        unimplemented!()
447    }
448
449    fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
450        unimplemented!()
451    }
452
453    fn diff_checkpoints(
454        &self,
455        _base_checkpoint: GitRepositoryCheckpoint,
456        _target_checkpoint: GitRepositoryCheckpoint,
457    ) -> BoxFuture<Result<String>> {
458        unimplemented!()
459    }
460
461    fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
462        unimplemented!()
463    }
464
465    fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
466        unimplemented!()
467    }
468}