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