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