1use crate::{FakeFs, FakeFsEntry, Fs, RemoveOptions, RenameOptions};
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, CommitDataReader, CommitDetails, CommitOptions, FetchOptions,
10 GRAPH_CHUNK_SIZE, GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder,
11 LogSource, PushOptions, Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree,
12 },
13 stash::GitStash,
14 status::{
15 DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
16 UnmergedStatus,
17 },
18};
19use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
20use ignore::gitignore::GitignoreBuilder;
21use parking_lot::Mutex;
22use rope::Rope;
23use smol::{channel::Sender, future::FutureExt as _};
24use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool};
25use text::LineEnding;
26use util::{paths::PathStyle, rel_path::RelPath};
27
28#[derive(Clone)]
29pub struct FakeGitRepository {
30 pub(crate) fs: Arc<FakeFs>,
31 pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
32 pub(crate) executor: BackgroundExecutor,
33 pub(crate) dot_git_path: PathBuf,
34 pub(crate) repository_dir_path: PathBuf,
35 pub(crate) common_dir_path: PathBuf,
36 pub(crate) is_trusted: Arc<AtomicBool>,
37}
38
39#[derive(Debug, Clone)]
40pub struct FakeCommitSnapshot {
41 pub head_contents: HashMap<RepoPath, String>,
42 pub index_contents: HashMap<RepoPath, String>,
43 pub sha: String,
44}
45
46#[derive(Debug, Clone)]
47pub struct FakeGitRepositoryState {
48 pub commit_history: Vec<FakeCommitSnapshot>,
49 pub event_emitter: smol::channel::Sender<PathBuf>,
50 pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
51 pub head_contents: HashMap<RepoPath, String>,
52 pub index_contents: HashMap<RepoPath, String>,
53 // everything in commit contents is in oids
54 pub merge_base_contents: HashMap<RepoPath, Oid>,
55 pub oids: HashMap<Oid, String>,
56 pub blames: HashMap<RepoPath, Blame>,
57 pub current_branch_name: Option<String>,
58 pub branches: HashSet<String>,
59 /// List of remotes, keys are names and values are URLs
60 pub remotes: HashMap<String, String>,
61 pub simulated_index_write_error_message: Option<String>,
62 pub simulated_create_worktree_error: Option<String>,
63 pub refs: HashMap<String, String>,
64 pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
65 pub stash_entries: GitStash,
66}
67
68impl FakeGitRepositoryState {
69 pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
70 FakeGitRepositoryState {
71 event_emitter,
72 head_contents: Default::default(),
73 index_contents: Default::default(),
74 unmerged_paths: Default::default(),
75 blames: Default::default(),
76 current_branch_name: Default::default(),
77 branches: Default::default(),
78 simulated_index_write_error_message: Default::default(),
79 simulated_create_worktree_error: Default::default(),
80 refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
81 merge_base_contents: Default::default(),
82 oids: Default::default(),
83 remotes: HashMap::default(),
84 graph_commits: Vec::new(),
85 commit_history: Vec::new(),
86 stash_entries: Default::default(),
87 }
88 }
89}
90
91impl FakeGitRepository {
92 fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
93 where
94 F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
95 T: Send,
96 {
97 let fs = self.fs.clone();
98 let executor = self.executor.clone();
99 let dot_git_path = self.dot_git_path.clone();
100 async move {
101 executor.simulate_random_delay().await;
102 fs.with_git_state(&dot_git_path, write, f)?
103 }
104 .boxed()
105 }
106}
107
108impl GitRepository for FakeGitRepository {
109 fn reload_index(&self) {}
110
111 fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
112 let fut = self.with_state_async(false, move |state| {
113 state
114 .index_contents
115 .get(&path)
116 .context("not present in index")
117 .cloned()
118 });
119 self.executor.spawn(async move { fut.await.ok() }).boxed()
120 }
121
122 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
123 let fut = self.with_state_async(false, move |state| {
124 state
125 .head_contents
126 .get(&path)
127 .context("not present in HEAD")
128 .cloned()
129 });
130 self.executor.spawn(async move { fut.await.ok() }).boxed()
131 }
132
133 fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
134 self.with_state_async(false, move |state| {
135 state.oids.get(&oid).cloned().context("oid does not exist")
136 })
137 .boxed()
138 }
139
140 fn load_commit(
141 &self,
142 _commit: String,
143 _cx: AsyncApp,
144 ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
145 unimplemented!()
146 }
147
148 fn set_index_text(
149 &self,
150 path: RepoPath,
151 content: Option<String>,
152 _env: Arc<HashMap<String, String>>,
153 _is_executable: bool,
154 ) -> BoxFuture<'_, anyhow::Result<()>> {
155 self.with_state_async(true, move |state| {
156 if let Some(message) = &state.simulated_index_write_error_message {
157 anyhow::bail!("{message}");
158 } else if let Some(content) = content {
159 state.index_contents.insert(path, content);
160 } else {
161 state.index_contents.remove(&path);
162 }
163 Ok(())
164 })
165 }
166
167 fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
168 let name = name.to_string();
169 let fut = self.with_state_async(false, move |state| {
170 state
171 .remotes
172 .get(&name)
173 .context("remote not found")
174 .cloned()
175 });
176 async move { fut.await.ok() }.boxed()
177 }
178
179 fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
180 let mut entries = HashMap::default();
181 self.with_state_async(false, |state| {
182 for (path, content) in &state.head_contents {
183 let status = if let Some((oid, original)) = state
184 .merge_base_contents
185 .get(path)
186 .map(|oid| (oid, &state.oids[oid]))
187 {
188 if original == content {
189 continue;
190 }
191 TreeDiffStatus::Modified { old: *oid }
192 } else {
193 TreeDiffStatus::Added
194 };
195 entries.insert(path.clone(), status);
196 }
197 for (path, oid) in &state.merge_base_contents {
198 if !entries.contains_key(path) {
199 entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
200 }
201 }
202 Ok(TreeDiff { entries })
203 })
204 .boxed()
205 }
206
207 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
208 self.with_state_async(false, |state| {
209 Ok(revs
210 .into_iter()
211 .map(|rev| state.refs.get(&rev).cloned())
212 .collect())
213 })
214 }
215
216 fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
217 async {
218 Ok(CommitDetails {
219 sha: commit.into(),
220 message: "initial commit".into(),
221 ..Default::default()
222 })
223 }
224 .boxed()
225 }
226
227 fn reset(
228 &self,
229 commit: String,
230 mode: ResetMode,
231 _env: Arc<HashMap<String, String>>,
232 ) -> BoxFuture<'_, Result<()>> {
233 self.with_state_async(true, move |state| {
234 let pop_count = if commit == "HEAD~" || commit == "HEAD^" {
235 1
236 } else if let Some(suffix) = commit.strip_prefix("HEAD~") {
237 suffix
238 .parse::<usize>()
239 .with_context(|| format!("Invalid HEAD~ offset: {commit}"))?
240 } else {
241 match state
242 .commit_history
243 .iter()
244 .rposition(|entry| entry.sha == commit)
245 {
246 Some(index) => state.commit_history.len() - index,
247 None => anyhow::bail!("Unknown commit ref: {commit}"),
248 }
249 };
250
251 if pop_count == 0 || pop_count > state.commit_history.len() {
252 anyhow::bail!(
253 "Cannot reset {pop_count} commit(s): only {} in history",
254 state.commit_history.len()
255 );
256 }
257
258 let target_index = state.commit_history.len() - pop_count;
259 let snapshot = state.commit_history[target_index].clone();
260 state.commit_history.truncate(target_index);
261
262 match mode {
263 ResetMode::Soft => {
264 state.head_contents = snapshot.head_contents;
265 }
266 ResetMode::Mixed => {
267 state.head_contents = snapshot.head_contents;
268 state.index_contents = state.head_contents.clone();
269 }
270 }
271
272 state.refs.insert("HEAD".into(), snapshot.sha);
273 Ok(())
274 })
275 }
276
277 fn checkout_files(
278 &self,
279 _commit: String,
280 _paths: Vec<RepoPath>,
281 _env: Arc<HashMap<String, String>>,
282 ) -> BoxFuture<'_, Result<()>> {
283 unimplemented!()
284 }
285
286 fn path(&self) -> PathBuf {
287 self.repository_dir_path.clone()
288 }
289
290 fn main_repository_path(&self) -> PathBuf {
291 self.common_dir_path.clone()
292 }
293
294 fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
295 async move { None }.boxed()
296 }
297
298 fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
299 let workdir_path = self.dot_git_path.parent().unwrap();
300
301 // Load gitignores
302 let ignores = workdir_path
303 .ancestors()
304 .filter_map(|dir| {
305 let ignore_path = dir.join(".gitignore");
306 let content = self.fs.read_file_sync(ignore_path).ok()?;
307 let content = String::from_utf8(content).ok()?;
308 let mut builder = GitignoreBuilder::new(dir);
309 for line in content.lines() {
310 builder.add_line(Some(dir.into()), line).ok()?;
311 }
312 builder.build().ok()
313 })
314 .collect::<Vec<_>>();
315
316 // Load working copy files.
317 let git_files: HashMap<RepoPath, (String, bool)> = self
318 .fs
319 .files()
320 .iter()
321 .filter_map(|path| {
322 // TODO better simulate git status output in the case of submodules and worktrees
323 let repo_path = path.strip_prefix(workdir_path).ok()?;
324 let mut is_ignored = repo_path.starts_with(".git");
325 for ignore in &ignores {
326 match ignore.matched_path_or_any_parents(path, false) {
327 ignore::Match::None => {}
328 ignore::Match::Ignore(_) => is_ignored = true,
329 ignore::Match::Whitelist(_) => break,
330 }
331 }
332 let content = self
333 .fs
334 .read_file_sync(path)
335 .ok()
336 .map(|content| String::from_utf8(content).unwrap())?;
337 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
338 Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
339 })
340 .collect();
341
342 let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
343 let mut entries = Vec::new();
344 let paths = state
345 .head_contents
346 .keys()
347 .chain(state.index_contents.keys())
348 .chain(git_files.keys())
349 .collect::<HashSet<_>>();
350 for path in paths {
351 if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
352 continue;
353 }
354
355 let head = state.head_contents.get(path);
356 let index = state.index_contents.get(path);
357 let unmerged = state.unmerged_paths.get(path);
358 let fs = git_files.get(path);
359 let status = match (unmerged, head, index, fs) {
360 (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
361 (_, Some(head), Some(index), Some((fs, _))) => {
362 FileStatus::Tracked(TrackedStatus {
363 index_status: if head == index {
364 StatusCode::Unmodified
365 } else {
366 StatusCode::Modified
367 },
368 worktree_status: if fs == index {
369 StatusCode::Unmodified
370 } else {
371 StatusCode::Modified
372 },
373 })
374 }
375 (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
376 index_status: if head == index {
377 StatusCode::Unmodified
378 } else {
379 StatusCode::Modified
380 },
381 worktree_status: StatusCode::Deleted,
382 }),
383 (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
384 index_status: StatusCode::Deleted,
385 worktree_status: StatusCode::Added,
386 }),
387 (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
388 index_status: StatusCode::Deleted,
389 worktree_status: StatusCode::Deleted,
390 }),
391 (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
392 index_status: StatusCode::Added,
393 worktree_status: if fs == index {
394 StatusCode::Unmodified
395 } else {
396 StatusCode::Modified
397 },
398 }),
399 (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
400 index_status: StatusCode::Added,
401 worktree_status: StatusCode::Deleted,
402 }),
403 (_, None, None, Some((_, is_ignored))) => {
404 if *is_ignored {
405 continue;
406 }
407 FileStatus::Untracked
408 }
409 (_, None, None, None) => {
410 unreachable!();
411 }
412 };
413 if status
414 != FileStatus::Tracked(TrackedStatus {
415 index_status: StatusCode::Unmodified,
416 worktree_status: StatusCode::Unmodified,
417 })
418 {
419 entries.push((path.clone(), status));
420 }
421 }
422 entries.sort_by(|a, b| a.0.cmp(&b.0));
423 anyhow::Ok(GitStatus {
424 entries: entries.into(),
425 })
426 });
427 Task::ready(match result {
428 Ok(result) => result,
429 Err(e) => Err(e),
430 })
431 }
432
433 fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
434 self.with_state_async(false, |state| Ok(state.stash_entries.clone()))
435 }
436
437 fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
438 self.with_state_async(false, move |state| {
439 let current_branch = &state.current_branch_name;
440 let mut branches = state
441 .branches
442 .iter()
443 .map(|branch_name| {
444 let ref_name = if branch_name.starts_with("refs/") {
445 branch_name.into()
446 } else if branch_name.contains('/') {
447 format!("refs/remotes/{branch_name}").into()
448 } else {
449 format!("refs/heads/{branch_name}").into()
450 };
451 Branch {
452 is_head: Some(branch_name) == current_branch.as_ref(),
453 ref_name,
454 most_recent_commit: None,
455 upstream: None,
456 }
457 })
458 .collect::<Vec<_>>();
459 // compute snapshot expects these to be sorted by ref_name
460 // because that's what git itself does
461 branches.sort_by(|a, b| a.ref_name.cmp(&b.ref_name));
462 Ok(branches)
463 })
464 }
465
466 fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
467 let fs = self.fs.clone();
468 let common_dir_path = self.common_dir_path.clone();
469 let executor = self.executor.clone();
470
471 async move {
472 executor.simulate_random_delay().await;
473
474 let (main_worktree, refs) = fs.with_git_state(&common_dir_path, false, |state| {
475 let work_dir = common_dir_path
476 .parent()
477 .map(PathBuf::from)
478 .unwrap_or_else(|| common_dir_path.clone());
479 let head_sha = state
480 .refs
481 .get("HEAD")
482 .cloned()
483 .unwrap_or_else(|| "0000000".to_string());
484 let branch_ref = state
485 .current_branch_name
486 .as_ref()
487 .map(|name| format!("refs/heads/{name}"))
488 .unwrap_or_else(|| "refs/heads/main".to_string());
489 let main_wt = Worktree {
490 path: work_dir,
491 ref_name: Some(branch_ref.into()),
492 sha: head_sha.into(),
493 is_main: true,
494 };
495 (main_wt, state.refs.clone())
496 })?;
497
498 let mut all = vec![main_worktree];
499
500 let worktrees_dir = common_dir_path.join("worktrees");
501 if let Ok(mut entries) = fs.read_dir(&worktrees_dir).await {
502 use futures::StreamExt;
503 while let Some(Ok(entry_path)) = entries.next().await {
504 let head_content = match fs.load(&entry_path.join("HEAD")).await {
505 Ok(content) => content,
506 Err(_) => continue,
507 };
508 let gitdir_content = match fs.load(&entry_path.join("gitdir")).await {
509 Ok(content) => content,
510 Err(_) => continue,
511 };
512
513 let ref_name = head_content
514 .strip_prefix("ref: ")
515 .map(|s| s.trim().to_string());
516 let sha = ref_name
517 .as_ref()
518 .and_then(|r| refs.get(r))
519 .cloned()
520 .unwrap_or_else(|| head_content.trim().to_string());
521
522 let worktree_path = PathBuf::from(gitdir_content.trim())
523 .parent()
524 .map(PathBuf::from)
525 .unwrap_or_default();
526
527 all.push(Worktree {
528 path: worktree_path,
529 ref_name: ref_name.map(Into::into),
530 sha: sha.into(),
531 is_main: false,
532 });
533 }
534 }
535
536 Ok(all)
537 }
538 .boxed()
539 }
540
541 fn create_worktree(
542 &self,
543 branch_name: Option<String>,
544 path: PathBuf,
545 from_commit: Option<String>,
546 ) -> BoxFuture<'_, Result<()>> {
547 let fs = self.fs.clone();
548 let executor = self.executor.clone();
549 let dot_git_path = self.dot_git_path.clone();
550 let common_dir_path = self.common_dir_path.clone();
551 async move {
552 executor.simulate_random_delay().await;
553 // Check for simulated error and duplicate branch before any side effects.
554 fs.with_git_state(&dot_git_path, false, |state| {
555 if let Some(message) = &state.simulated_create_worktree_error {
556 anyhow::bail!("{message}");
557 }
558 if let Some(ref name) = branch_name {
559 if state.branches.contains(name) {
560 bail!("a branch named '{}' already exists", name);
561 }
562 }
563 Ok(())
564 })??;
565
566 // Create the worktree checkout directory.
567 fs.create_dir(&path).await?;
568
569 // Create .git/worktrees/<name>/ directory with HEAD, commondir, gitdir.
570 let worktree_entry_name = branch_name
571 .as_deref()
572 .unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap());
573 let worktrees_entry_dir = common_dir_path.join("worktrees").join(worktree_entry_name);
574 fs.create_dir(&worktrees_entry_dir).await?;
575
576 let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
577 let head_content = if let Some(ref branch_name) = branch_name {
578 let ref_name = format!("refs/heads/{branch_name}");
579 format!("ref: {ref_name}")
580 } else {
581 sha.clone()
582 };
583 fs.write_file_internal(
584 worktrees_entry_dir.join("HEAD"),
585 head_content.into_bytes(),
586 false,
587 )?;
588 fs.write_file_internal(
589 worktrees_entry_dir.join("commondir"),
590 common_dir_path.to_string_lossy().into_owned().into_bytes(),
591 false,
592 )?;
593 let worktree_dot_git = path.join(".git");
594 fs.write_file_internal(
595 worktrees_entry_dir.join("gitdir"),
596 worktree_dot_git.to_string_lossy().into_owned().into_bytes(),
597 false,
598 )?;
599
600 // Create .git file in the worktree checkout.
601 fs.write_file_internal(
602 &worktree_dot_git,
603 format!("gitdir: {}", worktrees_entry_dir.display()).into_bytes(),
604 false,
605 )?;
606
607 // Update git state: add ref and branch.
608 fs.with_git_state(&dot_git_path, true, move |state| {
609 if let Some(branch_name) = branch_name {
610 let ref_name = format!("refs/heads/{branch_name}");
611 state.refs.insert(ref_name, sha);
612 state.branches.insert(branch_name);
613 }
614 Ok::<(), anyhow::Error>(())
615 })??;
616 Ok(())
617 }
618 .boxed()
619 }
620
621 fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
622 let fs = self.fs.clone();
623 let executor = self.executor.clone();
624 let common_dir_path = self.common_dir_path.clone();
625 async move {
626 executor.simulate_random_delay().await;
627
628 // Read the worktree's .git file to find its entry directory.
629 let dot_git_file = path.join(".git");
630 let content = fs
631 .load(&dot_git_file)
632 .await
633 .with_context(|| format!("no worktree found at path: {}", path.display()))?;
634 let gitdir = content
635 .strip_prefix("gitdir:")
636 .context("invalid .git file in worktree")?
637 .trim();
638 let worktree_entry_dir = PathBuf::from(gitdir);
639
640 // Remove the worktree checkout directory.
641 fs.remove_dir(
642 &path,
643 RemoveOptions {
644 recursive: true,
645 ignore_if_not_exists: false,
646 },
647 )
648 .await?;
649
650 // Remove the .git/worktrees/<name>/ directory.
651 fs.remove_dir(
652 &worktree_entry_dir,
653 RemoveOptions {
654 recursive: true,
655 ignore_if_not_exists: false,
656 },
657 )
658 .await?;
659
660 // Emit a git event on the main .git directory so the scanner
661 // notices the change.
662 fs.with_git_state(&common_dir_path, true, |_| {})?;
663
664 Ok(())
665 }
666 .boxed()
667 }
668
669 fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
670 let fs = self.fs.clone();
671 let executor = self.executor.clone();
672 let common_dir_path = self.common_dir_path.clone();
673 async move {
674 executor.simulate_random_delay().await;
675
676 // Read the worktree's .git file to find its entry directory.
677 let dot_git_file = old_path.join(".git");
678 let content = fs
679 .load(&dot_git_file)
680 .await
681 .with_context(|| format!("no worktree found at path: {}", old_path.display()))?;
682 let gitdir = content
683 .strip_prefix("gitdir:")
684 .context("invalid .git file in worktree")?
685 .trim();
686 let worktree_entry_dir = PathBuf::from(gitdir);
687
688 // Move the worktree checkout directory.
689 fs.rename(
690 &old_path,
691 &new_path,
692 RenameOptions {
693 overwrite: false,
694 ignore_if_exists: false,
695 create_parents: true,
696 },
697 )
698 .await?;
699
700 // Update the gitdir file in .git/worktrees/<name>/ to point to the
701 // new location.
702 let new_dot_git = new_path.join(".git");
703 fs.write_file_internal(
704 worktree_entry_dir.join("gitdir"),
705 new_dot_git.to_string_lossy().into_owned().into_bytes(),
706 false,
707 )?;
708
709 // Update the .git file in the moved worktree checkout.
710 fs.write_file_internal(
711 &new_dot_git,
712 format!("gitdir: {}", worktree_entry_dir.display()).into_bytes(),
713 false,
714 )?;
715
716 // Emit a git event on the main .git directory so the scanner
717 // notices the change.
718 fs.with_git_state(&common_dir_path, true, |_| {})?;
719
720 Ok(())
721 }
722 .boxed()
723 }
724
725 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
726 self.with_state_async(true, |state| {
727 state.current_branch_name = Some(name);
728 Ok(())
729 })
730 }
731
732 fn create_branch(
733 &self,
734 name: String,
735 _base_branch: Option<String>,
736 ) -> BoxFuture<'_, Result<()>> {
737 self.with_state_async(true, move |state| {
738 if let Some((remote, _)) = name.split_once('/')
739 && !state.remotes.contains_key(remote)
740 {
741 state.remotes.insert(remote.to_owned(), "".to_owned());
742 }
743 state.branches.insert(name);
744 Ok(())
745 })
746 }
747
748 fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
749 self.with_state_async(true, move |state| {
750 if !state.branches.remove(&branch) {
751 bail!("no such branch: {branch}");
752 }
753 state.branches.insert(new_name.clone());
754 if state.current_branch_name == Some(branch) {
755 state.current_branch_name = Some(new_name);
756 }
757 Ok(())
758 })
759 }
760
761 fn delete_branch(&self, _is_remote: bool, name: String) -> BoxFuture<'_, Result<()>> {
762 self.with_state_async(true, move |state| {
763 if !state.branches.remove(&name) {
764 bail!("no such branch: {name}");
765 }
766 Ok(())
767 })
768 }
769
770 fn blame(
771 &self,
772 path: RepoPath,
773 _content: Rope,
774 _line_ending: LineEnding,
775 ) -> BoxFuture<'_, Result<git::blame::Blame>> {
776 self.with_state_async(false, move |state| {
777 state
778 .blames
779 .get(&path)
780 .with_context(|| format!("failed to get blame for {:?}", path))
781 .cloned()
782 })
783 }
784
785 fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
786 self.file_history_paginated(path, 0, None)
787 }
788
789 fn file_history_paginated(
790 &self,
791 path: RepoPath,
792 _skip: usize,
793 _limit: Option<usize>,
794 ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
795 async move {
796 Ok(git::repository::FileHistory {
797 entries: Vec::new(),
798 path,
799 })
800 }
801 .boxed()
802 }
803
804 fn stage_paths(
805 &self,
806 paths: Vec<RepoPath>,
807 _env: Arc<HashMap<String, String>>,
808 ) -> BoxFuture<'_, Result<()>> {
809 Box::pin(async move {
810 let contents = paths
811 .into_iter()
812 .map(|path| {
813 let abs_path = self
814 .dot_git_path
815 .parent()
816 .unwrap()
817 .join(&path.as_std_path());
818 Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
819 })
820 .collect::<Vec<_>>();
821 let contents = join_all(contents).await;
822 self.with_state_async(true, move |state| {
823 for (path, content) in contents {
824 if let Some(content) = content {
825 state.index_contents.insert(path, content);
826 } else {
827 state.index_contents.remove(&path);
828 }
829 }
830 Ok(())
831 })
832 .await
833 })
834 }
835
836 fn unstage_paths(
837 &self,
838 paths: Vec<RepoPath>,
839 _env: Arc<HashMap<String, String>>,
840 ) -> BoxFuture<'_, Result<()>> {
841 self.with_state_async(true, move |state| {
842 for path in paths {
843 match state.head_contents.get(&path) {
844 Some(content) => state.index_contents.insert(path, content.clone()),
845 None => state.index_contents.remove(&path),
846 };
847 }
848 Ok(())
849 })
850 }
851
852 fn stash_paths(
853 &self,
854 _paths: Vec<RepoPath>,
855 _env: Arc<HashMap<String, String>>,
856 ) -> BoxFuture<'_, Result<()>> {
857 unimplemented!()
858 }
859
860 fn stash_pop(
861 &self,
862 _index: Option<usize>,
863 _env: Arc<HashMap<String, String>>,
864 ) -> BoxFuture<'_, Result<()>> {
865 unimplemented!()
866 }
867
868 fn stash_apply(
869 &self,
870 _index: Option<usize>,
871 _env: Arc<HashMap<String, String>>,
872 ) -> BoxFuture<'_, Result<()>> {
873 unimplemented!()
874 }
875
876 fn stash_drop(
877 &self,
878 _index: Option<usize>,
879 _env: Arc<HashMap<String, String>>,
880 ) -> BoxFuture<'_, Result<()>> {
881 unimplemented!()
882 }
883
884 fn commit(
885 &self,
886 _message: gpui::SharedString,
887 _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
888 options: CommitOptions,
889 _askpass: AskPassDelegate,
890 _env: Arc<HashMap<String, String>>,
891 ) -> BoxFuture<'_, Result<()>> {
892 self.with_state_async(true, move |state| {
893 if !options.allow_empty && !options.amend && state.index_contents == state.head_contents
894 {
895 anyhow::bail!("nothing to commit (use allow_empty to create an empty commit)");
896 }
897
898 let old_sha = state.refs.get("HEAD").cloned().unwrap_or_default();
899 state.commit_history.push(FakeCommitSnapshot {
900 head_contents: state.head_contents.clone(),
901 index_contents: state.index_contents.clone(),
902 sha: old_sha,
903 });
904
905 state.head_contents = state.index_contents.clone();
906
907 let new_sha = format!("fake-commit-{}", state.commit_history.len());
908 state.refs.insert("HEAD".into(), new_sha);
909
910 Ok(())
911 })
912 }
913
914 fn run_hook(
915 &self,
916 _hook: RunHook,
917 _env: Arc<HashMap<String, String>>,
918 ) -> BoxFuture<'_, Result<()>> {
919 async { Ok(()) }.boxed()
920 }
921
922 fn push(
923 &self,
924 _branch: String,
925 _remote_branch: String,
926 _remote: String,
927 _options: Option<PushOptions>,
928 _askpass: AskPassDelegate,
929 _env: Arc<HashMap<String, String>>,
930 _cx: AsyncApp,
931 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
932 unimplemented!()
933 }
934
935 fn pull(
936 &self,
937 _branch: Option<String>,
938 _remote: String,
939 _rebase: bool,
940 _askpass: AskPassDelegate,
941 _env: Arc<HashMap<String, String>>,
942 _cx: AsyncApp,
943 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
944 unimplemented!()
945 }
946
947 fn fetch(
948 &self,
949 _fetch_options: FetchOptions,
950 _askpass: AskPassDelegate,
951 _env: Arc<HashMap<String, String>>,
952 _cx: AsyncApp,
953 ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
954 unimplemented!()
955 }
956
957 fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
958 self.with_state_async(false, move |state| {
959 let remotes = state
960 .remotes
961 .keys()
962 .map(|r| Remote {
963 name: r.clone().into(),
964 })
965 .collect::<Vec<_>>();
966 Ok(remotes)
967 })
968 }
969
970 fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
971 unimplemented!()
972 }
973
974 fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
975 unimplemented!()
976 }
977
978 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
979 future::ready(Ok(Vec::new())).boxed()
980 }
981
982 fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
983 future::ready(Ok(String::new())).boxed()
984 }
985
986 fn diff_stat(
987 &self,
988 path_prefixes: &[RepoPath],
989 ) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
990 fn count_lines(s: &str) -> u32 {
991 if s.is_empty() {
992 0
993 } else {
994 s.lines().count() as u32
995 }
996 }
997
998 fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
999 if prefixes.is_empty() {
1000 return true;
1001 }
1002 prefixes.iter().any(|prefix| {
1003 let prefix_str = prefix.as_unix_str();
1004 if prefix_str == "." {
1005 return true;
1006 }
1007 path == prefix || path.starts_with(&prefix)
1008 })
1009 }
1010
1011 let path_prefixes = path_prefixes.to_vec();
1012
1013 let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
1014 let worktree_files: HashMap<RepoPath, String> = self
1015 .fs
1016 .files()
1017 .iter()
1018 .filter_map(|path| {
1019 let repo_path = path.strip_prefix(&workdir_path).ok()?;
1020 if repo_path.starts_with(".git") {
1021 return None;
1022 }
1023 let content = self
1024 .fs
1025 .read_file_sync(path)
1026 .ok()
1027 .and_then(|bytes| String::from_utf8(bytes).ok())?;
1028 let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
1029 Some((RepoPath::from_rel_path(&repo_path), content))
1030 })
1031 .collect();
1032
1033 self.with_state_async(false, move |state| {
1034 let mut entries = Vec::new();
1035 let all_paths: HashSet<&RepoPath> = state
1036 .head_contents
1037 .keys()
1038 .chain(
1039 worktree_files
1040 .keys()
1041 .filter(|p| state.index_contents.contains_key(*p)),
1042 )
1043 .collect();
1044 for path in all_paths {
1045 if !matches_prefixes(path, &path_prefixes) {
1046 continue;
1047 }
1048 let head = state.head_contents.get(path);
1049 let worktree = worktree_files.get(path);
1050 match (head, worktree) {
1051 (Some(old), Some(new)) if old != new => {
1052 entries.push((
1053 path.clone(),
1054 git::status::DiffStat {
1055 added: count_lines(new),
1056 deleted: count_lines(old),
1057 },
1058 ));
1059 }
1060 (Some(old), None) => {
1061 entries.push((
1062 path.clone(),
1063 git::status::DiffStat {
1064 added: 0,
1065 deleted: count_lines(old),
1066 },
1067 ));
1068 }
1069 (None, Some(new)) => {
1070 entries.push((
1071 path.clone(),
1072 git::status::DiffStat {
1073 added: count_lines(new),
1074 deleted: 0,
1075 },
1076 ));
1077 }
1078 _ => {}
1079 }
1080 }
1081 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1082 Ok(git::status::GitDiffStat {
1083 entries: entries.into(),
1084 })
1085 })
1086 .boxed()
1087 }
1088
1089 fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
1090 let executor = self.executor.clone();
1091 let fs = self.fs.clone();
1092 let checkpoints = self.checkpoints.clone();
1093 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
1094 async move {
1095 executor.simulate_random_delay().await;
1096 let oid = git::Oid::random(&mut *executor.rng().lock());
1097 let entry = fs.entry(&repository_dir_path)?;
1098 checkpoints.lock().insert(oid, entry);
1099 Ok(GitRepositoryCheckpoint { commit_sha: oid })
1100 }
1101 .boxed()
1102 }
1103
1104 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
1105 let executor = self.executor.clone();
1106 let fs = self.fs.clone();
1107 let checkpoints = self.checkpoints.clone();
1108 let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
1109 async move {
1110 executor.simulate_random_delay().await;
1111 let checkpoints = checkpoints.lock();
1112 let entry = checkpoints
1113 .get(&checkpoint.commit_sha)
1114 .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
1115 fs.insert_entry(&repository_dir_path, entry.clone())?;
1116 Ok(())
1117 }
1118 .boxed()
1119 }
1120
1121 fn compare_checkpoints(
1122 &self,
1123 left: GitRepositoryCheckpoint,
1124 right: GitRepositoryCheckpoint,
1125 ) -> BoxFuture<'_, Result<bool>> {
1126 let executor = self.executor.clone();
1127 let checkpoints = self.checkpoints.clone();
1128 async move {
1129 executor.simulate_random_delay().await;
1130 let checkpoints = checkpoints.lock();
1131 let left = checkpoints
1132 .get(&left.commit_sha)
1133 .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
1134 let right = checkpoints
1135 .get(&right.commit_sha)
1136 .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
1137
1138 Ok(left == right)
1139 }
1140 .boxed()
1141 }
1142
1143 fn diff_checkpoints(
1144 &self,
1145 base_checkpoint: GitRepositoryCheckpoint,
1146 target_checkpoint: GitRepositoryCheckpoint,
1147 ) -> BoxFuture<'_, Result<String>> {
1148 let executor = self.executor.clone();
1149 let checkpoints = self.checkpoints.clone();
1150 async move {
1151 executor.simulate_random_delay().await;
1152 let checkpoints = checkpoints.lock();
1153 let base = checkpoints
1154 .get(&base_checkpoint.commit_sha)
1155 .context(format!(
1156 "invalid base checkpoint: {}",
1157 base_checkpoint.commit_sha
1158 ))?;
1159 let target = checkpoints
1160 .get(&target_checkpoint.commit_sha)
1161 .context(format!(
1162 "invalid target checkpoint: {}",
1163 target_checkpoint.commit_sha
1164 ))?;
1165
1166 fn collect_files(
1167 entry: &FakeFsEntry,
1168 prefix: String,
1169 out: &mut std::collections::BTreeMap<String, String>,
1170 ) {
1171 match entry {
1172 FakeFsEntry::File { content, .. } => {
1173 out.insert(prefix, String::from_utf8_lossy(content).into_owned());
1174 }
1175 FakeFsEntry::Dir { entries, .. } => {
1176 for (name, child) in entries {
1177 let path = if prefix.is_empty() {
1178 name.clone()
1179 } else {
1180 format!("{prefix}/{name}")
1181 };
1182 collect_files(child, path, out);
1183 }
1184 }
1185 FakeFsEntry::Symlink { .. } => {}
1186 }
1187 }
1188
1189 let mut base_files = std::collections::BTreeMap::new();
1190 let mut target_files = std::collections::BTreeMap::new();
1191 collect_files(base, String::new(), &mut base_files);
1192 collect_files(target, String::new(), &mut target_files);
1193
1194 let all_paths: std::collections::BTreeSet<&String> =
1195 base_files.keys().chain(target_files.keys()).collect();
1196
1197 let mut diff = String::new();
1198 for path in all_paths {
1199 match (base_files.get(path), target_files.get(path)) {
1200 (Some(base_content), Some(target_content))
1201 if base_content != target_content =>
1202 {
1203 diff.push_str(&format!("diff --git a/{path} b/{path}\n"));
1204 diff.push_str(&format!("--- a/{path}\n"));
1205 diff.push_str(&format!("+++ b/{path}\n"));
1206 for line in base_content.lines() {
1207 diff.push_str(&format!("-{line}\n"));
1208 }
1209 for line in target_content.lines() {
1210 diff.push_str(&format!("+{line}\n"));
1211 }
1212 }
1213 (Some(_), None) => {
1214 diff.push_str(&format!("diff --git a/{path} /dev/null\n"));
1215 diff.push_str("deleted file\n");
1216 }
1217 (None, Some(_)) => {
1218 diff.push_str(&format!("diff --git /dev/null b/{path}\n"));
1219 diff.push_str("new file\n");
1220 }
1221 _ => {}
1222 }
1223 }
1224 Ok(diff)
1225 }
1226 .boxed()
1227 }
1228
1229 fn default_branch(
1230 &self,
1231 include_remote_name: bool,
1232 ) -> BoxFuture<'_, Result<Option<SharedString>>> {
1233 async move {
1234 Ok(Some(if include_remote_name {
1235 "origin/main".into()
1236 } else {
1237 "main".into()
1238 }))
1239 }
1240 .boxed()
1241 }
1242
1243 fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
1244 self.with_state_async(true, move |state| {
1245 state.remotes.insert(name, url);
1246 Ok(())
1247 })
1248 }
1249
1250 fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
1251 self.with_state_async(true, move |state| {
1252 state.branches.retain(|branch| {
1253 branch
1254 .split_once('/')
1255 .is_none_or(|(remote, _)| remote != name)
1256 });
1257 state.remotes.remove(&name);
1258 Ok(())
1259 })
1260 }
1261
1262 fn initial_graph_data(
1263 &self,
1264 _log_source: LogSource,
1265 _log_order: LogOrder,
1266 request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
1267 ) -> BoxFuture<'_, Result<()>> {
1268 let fs = self.fs.clone();
1269 let dot_git_path = self.dot_git_path.clone();
1270 async move {
1271 let graph_commits =
1272 fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
1273
1274 for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1275 request_tx.send(chunk.to_vec()).await.ok();
1276 }
1277 Ok(())
1278 }
1279 .boxed()
1280 }
1281
1282 fn search_commits(
1283 &self,
1284 _log_source: LogSource,
1285 _search_args: SearchCommitArgs,
1286 _request_tx: Sender<Oid>,
1287 ) -> BoxFuture<'_, Result<()>> {
1288 async { bail!("search_commits not supported for FakeGitRepository") }.boxed()
1289 }
1290
1291 fn commit_data_reader(&self) -> Result<CommitDataReader> {
1292 anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
1293 }
1294
1295 fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> {
1296 self.with_state_async(true, move |state| {
1297 state.refs.insert(ref_name, commit);
1298 Ok(())
1299 })
1300 }
1301
1302 fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> {
1303 self.with_state_async(true, move |state| {
1304 state.refs.remove(&ref_name);
1305 Ok(())
1306 })
1307 }
1308
1309 fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {
1310 async { Ok(()) }.boxed()
1311 }
1312
1313 fn set_trusted(&self, trusted: bool) {
1314 self.is_trusted
1315 .store(trusted, std::sync::atomic::Ordering::Release);
1316 }
1317
1318 fn is_trusted(&self) -> bool {
1319 self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1320 }
1321}