fake_git_repo.rs

   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, Worktree,
  12    },
  13    status::{
  14        DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
  15        UnmergedStatus,
  16    },
  17};
  18use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
  19use ignore::gitignore::GitignoreBuilder;
  20use parking_lot::Mutex;
  21use rope::Rope;
  22use smol::{channel::Sender, future::FutureExt as _};
  23use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool};
  24use text::LineEnding;
  25use util::{paths::PathStyle, rel_path::RelPath};
  26
  27#[derive(Clone)]
  28pub struct FakeGitRepository {
  29    pub(crate) fs: Arc<FakeFs>,
  30    pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
  31    pub(crate) executor: BackgroundExecutor,
  32    pub(crate) dot_git_path: PathBuf,
  33    pub(crate) repository_dir_path: PathBuf,
  34    pub(crate) common_dir_path: PathBuf,
  35    pub(crate) is_trusted: Arc<AtomicBool>,
  36}
  37
  38#[derive(Debug, Clone)]
  39pub struct FakeGitRepositoryState {
  40    pub event_emitter: smol::channel::Sender<PathBuf>,
  41    pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
  42    pub head_contents: HashMap<RepoPath, String>,
  43    pub index_contents: HashMap<RepoPath, String>,
  44    // everything in commit contents is in oids
  45    pub merge_base_contents: HashMap<RepoPath, Oid>,
  46    pub oids: HashMap<Oid, String>,
  47    pub blames: HashMap<RepoPath, Blame>,
  48    pub current_branch_name: Option<String>,
  49    pub branches: HashSet<String>,
  50    /// List of remotes, keys are names and values are URLs
  51    pub remotes: HashMap<String, String>,
  52    pub simulated_index_write_error_message: Option<String>,
  53    pub simulated_create_worktree_error: Option<String>,
  54    pub refs: HashMap<String, String>,
  55    pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
  56    pub worktrees: Vec<Worktree>,
  57}
  58
  59impl FakeGitRepositoryState {
  60    pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
  61        FakeGitRepositoryState {
  62            event_emitter,
  63            head_contents: Default::default(),
  64            index_contents: Default::default(),
  65            unmerged_paths: Default::default(),
  66            blames: Default::default(),
  67            current_branch_name: Default::default(),
  68            branches: Default::default(),
  69            simulated_index_write_error_message: Default::default(),
  70            simulated_create_worktree_error: Default::default(),
  71            refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
  72            merge_base_contents: Default::default(),
  73            oids: Default::default(),
  74            remotes: HashMap::default(),
  75            graph_commits: Vec::new(),
  76            worktrees: Vec::new(),
  77        }
  78    }
  79}
  80
  81impl FakeGitRepository {
  82    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
  83    where
  84        F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
  85        T: Send,
  86    {
  87        let fs = self.fs.clone();
  88        let executor = self.executor.clone();
  89        let dot_git_path = self.dot_git_path.clone();
  90        async move {
  91            executor.simulate_random_delay().await;
  92            fs.with_git_state(&dot_git_path, write, f)?
  93        }
  94        .boxed()
  95    }
  96}
  97
  98impl GitRepository for FakeGitRepository {
  99    fn reload_index(&self) {}
 100
 101    fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
 102        let fut = self.with_state_async(false, move |state| {
 103            state
 104                .index_contents
 105                .get(&path)
 106                .context("not present in index")
 107                .cloned()
 108        });
 109        self.executor.spawn(async move { fut.await.ok() }).boxed()
 110    }
 111
 112    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
 113        let fut = self.with_state_async(false, move |state| {
 114            state
 115                .head_contents
 116                .get(&path)
 117                .context("not present in HEAD")
 118                .cloned()
 119        });
 120        self.executor.spawn(async move { fut.await.ok() }).boxed()
 121    }
 122
 123    fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
 124        self.with_state_async(false, move |state| {
 125            state.oids.get(&oid).cloned().context("oid does not exist")
 126        })
 127        .boxed()
 128    }
 129
 130    fn load_commit(
 131        &self,
 132        _commit: String,
 133        _cx: AsyncApp,
 134    ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
 135        unimplemented!()
 136    }
 137
 138    fn set_index_text(
 139        &self,
 140        path: RepoPath,
 141        content: Option<String>,
 142        _env: Arc<HashMap<String, String>>,
 143        _is_executable: bool,
 144    ) -> BoxFuture<'_, anyhow::Result<()>> {
 145        self.with_state_async(true, move |state| {
 146            if let Some(message) = &state.simulated_index_write_error_message {
 147                anyhow::bail!("{message}");
 148            } else if let Some(content) = content {
 149                state.index_contents.insert(path, content);
 150            } else {
 151                state.index_contents.remove(&path);
 152            }
 153            Ok(())
 154        })
 155    }
 156
 157    fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
 158        let name = name.to_string();
 159        let fut = self.with_state_async(false, move |state| {
 160            state
 161                .remotes
 162                .get(&name)
 163                .context("remote not found")
 164                .cloned()
 165        });
 166        async move { fut.await.ok() }.boxed()
 167    }
 168
 169    fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
 170        let mut entries = HashMap::default();
 171        self.with_state_async(false, |state| {
 172            for (path, content) in &state.head_contents {
 173                let status = if let Some((oid, original)) = state
 174                    .merge_base_contents
 175                    .get(path)
 176                    .map(|oid| (oid, &state.oids[oid]))
 177                {
 178                    if original == content {
 179                        continue;
 180                    }
 181                    TreeDiffStatus::Modified { old: *oid }
 182                } else {
 183                    TreeDiffStatus::Added
 184                };
 185                entries.insert(path.clone(), status);
 186            }
 187            for (path, oid) in &state.merge_base_contents {
 188                if !entries.contains_key(path) {
 189                    entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
 190                }
 191            }
 192            Ok(TreeDiff { entries })
 193        })
 194        .boxed()
 195    }
 196
 197    fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
 198        self.with_state_async(false, |state| {
 199            Ok(revs
 200                .into_iter()
 201                .map(|rev| state.refs.get(&rev).cloned())
 202                .collect())
 203        })
 204    }
 205
 206    fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
 207        async {
 208            Ok(CommitDetails {
 209                sha: commit.into(),
 210                message: "initial commit".into(),
 211                ..Default::default()
 212            })
 213        }
 214        .boxed()
 215    }
 216
 217    fn reset(
 218        &self,
 219        _commit: String,
 220        _mode: ResetMode,
 221        _env: Arc<HashMap<String, String>>,
 222    ) -> BoxFuture<'_, Result<()>> {
 223        unimplemented!()
 224    }
 225
 226    fn checkout_files(
 227        &self,
 228        _commit: String,
 229        _paths: Vec<RepoPath>,
 230        _env: Arc<HashMap<String, String>>,
 231    ) -> BoxFuture<'_, Result<()>> {
 232        unimplemented!()
 233    }
 234
 235    fn path(&self) -> PathBuf {
 236        self.repository_dir_path.clone()
 237    }
 238
 239    fn main_repository_path(&self) -> PathBuf {
 240        self.common_dir_path.clone()
 241    }
 242
 243    fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
 244        async move { None }.boxed()
 245    }
 246
 247    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
 248        let workdir_path = self.dot_git_path.parent().unwrap();
 249
 250        // Load gitignores
 251        let ignores = workdir_path
 252            .ancestors()
 253            .filter_map(|dir| {
 254                let ignore_path = dir.join(".gitignore");
 255                let content = self.fs.read_file_sync(ignore_path).ok()?;
 256                let content = String::from_utf8(content).ok()?;
 257                let mut builder = GitignoreBuilder::new(dir);
 258                for line in content.lines() {
 259                    builder.add_line(Some(dir.into()), line).ok()?;
 260                }
 261                builder.build().ok()
 262            })
 263            .collect::<Vec<_>>();
 264
 265        // Load working copy files.
 266        let git_files: HashMap<RepoPath, (String, bool)> = self
 267            .fs
 268            .files()
 269            .iter()
 270            .filter_map(|path| {
 271                // TODO better simulate git status output in the case of submodules and worktrees
 272                let repo_path = path.strip_prefix(workdir_path).ok()?;
 273                let mut is_ignored = repo_path.starts_with(".git");
 274                for ignore in &ignores {
 275                    match ignore.matched_path_or_any_parents(path, false) {
 276                        ignore::Match::None => {}
 277                        ignore::Match::Ignore(_) => is_ignored = true,
 278                        ignore::Match::Whitelist(_) => break,
 279                    }
 280                }
 281                let content = self
 282                    .fs
 283                    .read_file_sync(path)
 284                    .ok()
 285                    .map(|content| String::from_utf8(content).unwrap())?;
 286                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
 287                Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
 288            })
 289            .collect();
 290
 291        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
 292            let mut entries = Vec::new();
 293            let paths = state
 294                .head_contents
 295                .keys()
 296                .chain(state.index_contents.keys())
 297                .chain(git_files.keys())
 298                .collect::<HashSet<_>>();
 299            for path in paths {
 300                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
 301                    continue;
 302                }
 303
 304                let head = state.head_contents.get(path);
 305                let index = state.index_contents.get(path);
 306                let unmerged = state.unmerged_paths.get(path);
 307                let fs = git_files.get(path);
 308                let status = match (unmerged, head, index, fs) {
 309                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
 310                    (_, Some(head), Some(index), Some((fs, _))) => {
 311                        FileStatus::Tracked(TrackedStatus {
 312                            index_status: if head == index {
 313                                StatusCode::Unmodified
 314                            } else {
 315                                StatusCode::Modified
 316                            },
 317                            worktree_status: if fs == index {
 318                                StatusCode::Unmodified
 319                            } else {
 320                                StatusCode::Modified
 321                            },
 322                        })
 323                    }
 324                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
 325                        index_status: if head == index {
 326                            StatusCode::Unmodified
 327                        } else {
 328                            StatusCode::Modified
 329                        },
 330                        worktree_status: StatusCode::Deleted,
 331                    }),
 332                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
 333                        index_status: StatusCode::Deleted,
 334                        worktree_status: StatusCode::Added,
 335                    }),
 336                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
 337                        index_status: StatusCode::Deleted,
 338                        worktree_status: StatusCode::Deleted,
 339                    }),
 340                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
 341                        index_status: StatusCode::Added,
 342                        worktree_status: if fs == index {
 343                            StatusCode::Unmodified
 344                        } else {
 345                            StatusCode::Modified
 346                        },
 347                    }),
 348                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
 349                        index_status: StatusCode::Added,
 350                        worktree_status: StatusCode::Deleted,
 351                    }),
 352                    (_, None, None, Some((_, is_ignored))) => {
 353                        if *is_ignored {
 354                            continue;
 355                        }
 356                        FileStatus::Untracked
 357                    }
 358                    (_, None, None, None) => {
 359                        unreachable!();
 360                    }
 361                };
 362                if status
 363                    != FileStatus::Tracked(TrackedStatus {
 364                        index_status: StatusCode::Unmodified,
 365                        worktree_status: StatusCode::Unmodified,
 366                    })
 367                {
 368                    entries.push((path.clone(), status));
 369                }
 370            }
 371            entries.sort_by(|a, b| a.0.cmp(&b.0));
 372            anyhow::Ok(GitStatus {
 373                entries: entries.into(),
 374            })
 375        });
 376        Task::ready(match result {
 377            Ok(result) => result,
 378            Err(e) => Err(e),
 379        })
 380    }
 381
 382    fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
 383        async { Ok(git::stash::GitStash::default()) }.boxed()
 384    }
 385
 386    fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
 387        self.with_state_async(false, move |state| {
 388            let current_branch = &state.current_branch_name;
 389            Ok(state
 390                .branches
 391                .iter()
 392                .map(|branch_name| {
 393                    let ref_name = if branch_name.starts_with("refs/") {
 394                        branch_name.into()
 395                    } else if branch_name.contains('/') {
 396                        format!("refs/remotes/{branch_name}").into()
 397                    } else {
 398                        format!("refs/heads/{branch_name}").into()
 399                    };
 400                    Branch {
 401                        is_head: Some(branch_name) == current_branch.as_ref(),
 402                        ref_name,
 403                        most_recent_commit: None,
 404                        upstream: None,
 405                    }
 406                })
 407                .collect())
 408        })
 409    }
 410
 411    fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
 412        let dot_git_path = self.dot_git_path.clone();
 413        self.with_state_async(false, move |state| {
 414            let work_dir = dot_git_path
 415                .parent()
 416                .map(PathBuf::from)
 417                .unwrap_or(dot_git_path);
 418            let head_sha = state
 419                .refs
 420                .get("HEAD")
 421                .cloned()
 422                .unwrap_or_else(|| "0000000".to_string());
 423            let branch_ref = state
 424                .current_branch_name
 425                .as_ref()
 426                .map(|name| format!("refs/heads/{name}"))
 427                .unwrap_or_else(|| "refs/heads/main".to_string());
 428            let main_worktree = Worktree {
 429                path: work_dir,
 430                ref_name: branch_ref.into(),
 431                sha: head_sha.into(),
 432            };
 433            let mut all = vec![main_worktree];
 434            all.extend(state.worktrees.iter().cloned());
 435            Ok(all)
 436        })
 437    }
 438
 439    fn create_worktree(
 440        &self,
 441        name: String,
 442        directory: PathBuf,
 443        from_commit: Option<String>,
 444    ) -> BoxFuture<'_, Result<()>> {
 445        let fs = self.fs.clone();
 446        let executor = self.executor.clone();
 447        let dot_git_path = self.dot_git_path.clone();
 448        async move {
 449            let path = directory.join(&name);
 450            executor.simulate_random_delay().await;
 451            // Check for simulated error before any side effects
 452            fs.with_git_state(&dot_git_path, false, |state| {
 453                if let Some(message) = &state.simulated_create_worktree_error {
 454                    anyhow::bail!("{message}");
 455                }
 456                Ok(())
 457            })??;
 458            // Create directory before updating state so state is never
 459            // inconsistent with the filesystem
 460            fs.create_dir(&path).await?;
 461            fs.with_git_state(&dot_git_path, true, {
 462                let path = path.clone();
 463                move |state| {
 464                    if state.branches.contains(&name) {
 465                        bail!("a branch named '{}' already exists", name);
 466                    }
 467                    let ref_name = format!("refs/heads/{name}");
 468                    let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
 469                    state.refs.insert(ref_name.clone(), sha.clone());
 470                    state.worktrees.push(Worktree {
 471                        path,
 472                        ref_name: ref_name.into(),
 473                        sha: sha.into(),
 474                    });
 475                    state.branches.insert(name);
 476                    Ok::<(), anyhow::Error>(())
 477                }
 478            })??;
 479            Ok(())
 480        }
 481        .boxed()
 482    }
 483
 484    fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
 485        let fs = self.fs.clone();
 486        let executor = self.executor.clone();
 487        let dot_git_path = self.dot_git_path.clone();
 488        async move {
 489            executor.simulate_random_delay().await;
 490            // Validate the worktree exists in state before touching the filesystem
 491            fs.with_git_state(&dot_git_path, false, {
 492                let path = path.clone();
 493                move |state| {
 494                    if !state.worktrees.iter().any(|w| w.path == path) {
 495                        bail!("no worktree found at path: {}", path.display());
 496                    }
 497                    Ok(())
 498                }
 499            })??;
 500            // Now remove the directory
 501            fs.remove_dir(
 502                &path,
 503                RemoveOptions {
 504                    recursive: true,
 505                    ignore_if_not_exists: false,
 506                },
 507            )
 508            .await?;
 509            // Update state
 510            fs.with_git_state(&dot_git_path, true, move |state| {
 511                state.worktrees.retain(|worktree| worktree.path != path);
 512                Ok::<(), anyhow::Error>(())
 513            })??;
 514            Ok(())
 515        }
 516        .boxed()
 517    }
 518
 519    fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
 520        let fs = self.fs.clone();
 521        let executor = self.executor.clone();
 522        let dot_git_path = self.dot_git_path.clone();
 523        async move {
 524            executor.simulate_random_delay().await;
 525            // Validate the worktree exists in state before touching the filesystem
 526            fs.with_git_state(&dot_git_path, false, {
 527                let old_path = old_path.clone();
 528                move |state| {
 529                    if !state.worktrees.iter().any(|w| w.path == old_path) {
 530                        bail!("no worktree found at path: {}", old_path.display());
 531                    }
 532                    Ok(())
 533                }
 534            })??;
 535            // Now move the directory
 536            fs.rename(
 537                &old_path,
 538                &new_path,
 539                RenameOptions {
 540                    overwrite: false,
 541                    ignore_if_exists: false,
 542                    create_parents: true,
 543                },
 544            )
 545            .await?;
 546            // Update state
 547            fs.with_git_state(&dot_git_path, true, move |state| {
 548                let worktree = state
 549                    .worktrees
 550                    .iter_mut()
 551                    .find(|worktree| worktree.path == old_path)
 552                    .expect("worktree was validated above");
 553                worktree.path = new_path;
 554                Ok::<(), anyhow::Error>(())
 555            })??;
 556            Ok(())
 557        }
 558        .boxed()
 559    }
 560
 561    fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
 562        self.with_state_async(true, |state| {
 563            state.current_branch_name = Some(name);
 564            Ok(())
 565        })
 566    }
 567
 568    fn create_branch(
 569        &self,
 570        name: String,
 571        _base_branch: Option<String>,
 572    ) -> BoxFuture<'_, Result<()>> {
 573        self.with_state_async(true, move |state| {
 574            if let Some((remote, _)) = name.split_once('/')
 575                && !state.remotes.contains_key(remote)
 576            {
 577                state.remotes.insert(remote.to_owned(), "".to_owned());
 578            }
 579            state.branches.insert(name);
 580            Ok(())
 581        })
 582    }
 583
 584    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
 585        self.with_state_async(true, move |state| {
 586            if !state.branches.remove(&branch) {
 587                bail!("no such branch: {branch}");
 588            }
 589            state.branches.insert(new_name.clone());
 590            if state.current_branch_name == Some(branch) {
 591                state.current_branch_name = Some(new_name);
 592            }
 593            Ok(())
 594        })
 595    }
 596
 597    fn delete_branch(&self, _is_remote: bool, name: String) -> BoxFuture<'_, Result<()>> {
 598        self.with_state_async(true, move |state| {
 599            if !state.branches.remove(&name) {
 600                bail!("no such branch: {name}");
 601            }
 602            Ok(())
 603        })
 604    }
 605
 606    fn blame(
 607        &self,
 608        path: RepoPath,
 609        _content: Rope,
 610        _line_ending: LineEnding,
 611    ) -> BoxFuture<'_, Result<git::blame::Blame>> {
 612        self.with_state_async(false, move |state| {
 613            state
 614                .blames
 615                .get(&path)
 616                .with_context(|| format!("failed to get blame for {:?}", path))
 617                .cloned()
 618        })
 619    }
 620
 621    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
 622        self.file_history_paginated(path, 0, None)
 623    }
 624
 625    fn file_history_paginated(
 626        &self,
 627        path: RepoPath,
 628        _skip: usize,
 629        _limit: Option<usize>,
 630    ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
 631        async move {
 632            Ok(git::repository::FileHistory {
 633                entries: Vec::new(),
 634                path,
 635            })
 636        }
 637        .boxed()
 638    }
 639
 640    fn stage_paths(
 641        &self,
 642        paths: Vec<RepoPath>,
 643        _env: Arc<HashMap<String, String>>,
 644    ) -> BoxFuture<'_, Result<()>> {
 645        Box::pin(async move {
 646            let contents = paths
 647                .into_iter()
 648                .map(|path| {
 649                    let abs_path = self
 650                        .dot_git_path
 651                        .parent()
 652                        .unwrap()
 653                        .join(&path.as_std_path());
 654                    Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
 655                })
 656                .collect::<Vec<_>>();
 657            let contents = join_all(contents).await;
 658            self.with_state_async(true, move |state| {
 659                for (path, content) in contents {
 660                    if let Some(content) = content {
 661                        state.index_contents.insert(path, content);
 662                    } else {
 663                        state.index_contents.remove(&path);
 664                    }
 665                }
 666                Ok(())
 667            })
 668            .await
 669        })
 670    }
 671
 672    fn unstage_paths(
 673        &self,
 674        paths: Vec<RepoPath>,
 675        _env: Arc<HashMap<String, String>>,
 676    ) -> BoxFuture<'_, Result<()>> {
 677        self.with_state_async(true, move |state| {
 678            for path in paths {
 679                match state.head_contents.get(&path) {
 680                    Some(content) => state.index_contents.insert(path, content.clone()),
 681                    None => state.index_contents.remove(&path),
 682                };
 683            }
 684            Ok(())
 685        })
 686    }
 687
 688    fn stash_paths(
 689        &self,
 690        _paths: Vec<RepoPath>,
 691        _env: Arc<HashMap<String, String>>,
 692    ) -> BoxFuture<'_, Result<()>> {
 693        unimplemented!()
 694    }
 695
 696    fn stash_pop(
 697        &self,
 698        _index: Option<usize>,
 699        _env: Arc<HashMap<String, String>>,
 700    ) -> BoxFuture<'_, Result<()>> {
 701        unimplemented!()
 702    }
 703
 704    fn stash_apply(
 705        &self,
 706        _index: Option<usize>,
 707        _env: Arc<HashMap<String, String>>,
 708    ) -> BoxFuture<'_, Result<()>> {
 709        unimplemented!()
 710    }
 711
 712    fn stash_drop(
 713        &self,
 714        _index: Option<usize>,
 715        _env: Arc<HashMap<String, String>>,
 716    ) -> BoxFuture<'_, Result<()>> {
 717        unimplemented!()
 718    }
 719
 720    fn commit(
 721        &self,
 722        _message: gpui::SharedString,
 723        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
 724        _options: CommitOptions,
 725        _askpass: AskPassDelegate,
 726        _env: Arc<HashMap<String, String>>,
 727    ) -> BoxFuture<'_, Result<()>> {
 728        async { Ok(()) }.boxed()
 729    }
 730
 731    fn run_hook(
 732        &self,
 733        _hook: RunHook,
 734        _env: Arc<HashMap<String, String>>,
 735    ) -> BoxFuture<'_, Result<()>> {
 736        async { Ok(()) }.boxed()
 737    }
 738
 739    fn push(
 740        &self,
 741        _branch: String,
 742        _remote_branch: String,
 743        _remote: String,
 744        _options: Option<PushOptions>,
 745        _askpass: AskPassDelegate,
 746        _env: Arc<HashMap<String, String>>,
 747        _cx: AsyncApp,
 748    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 749        unimplemented!()
 750    }
 751
 752    fn pull(
 753        &self,
 754        _branch: Option<String>,
 755        _remote: String,
 756        _rebase: bool,
 757        _askpass: AskPassDelegate,
 758        _env: Arc<HashMap<String, String>>,
 759        _cx: AsyncApp,
 760    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 761        unimplemented!()
 762    }
 763
 764    fn fetch(
 765        &self,
 766        _fetch_options: FetchOptions,
 767        _askpass: AskPassDelegate,
 768        _env: Arc<HashMap<String, String>>,
 769        _cx: AsyncApp,
 770    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 771        unimplemented!()
 772    }
 773
 774    fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
 775        self.with_state_async(false, move |state| {
 776            let remotes = state
 777                .remotes
 778                .keys()
 779                .map(|r| Remote {
 780                    name: r.clone().into(),
 781                })
 782                .collect::<Vec<_>>();
 783            Ok(remotes)
 784        })
 785    }
 786
 787    fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
 788        unimplemented!()
 789    }
 790
 791    fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
 792        unimplemented!()
 793    }
 794
 795    fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
 796        future::ready(Ok(Vec::new())).boxed()
 797    }
 798
 799    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
 800        future::ready(Ok(String::new())).boxed()
 801    }
 802
 803    fn diff_stat(
 804        &self,
 805        path_prefixes: &[RepoPath],
 806    ) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
 807        fn count_lines(s: &str) -> u32 {
 808            if s.is_empty() {
 809                0
 810            } else {
 811                s.lines().count() as u32
 812            }
 813        }
 814
 815        fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
 816            if prefixes.is_empty() {
 817                return true;
 818            }
 819            prefixes.iter().any(|prefix| {
 820                let prefix_str = prefix.as_unix_str();
 821                if prefix_str == "." {
 822                    return true;
 823                }
 824                path == prefix || path.starts_with(&prefix)
 825            })
 826        }
 827
 828        let path_prefixes = path_prefixes.to_vec();
 829
 830        let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
 831        let worktree_files: HashMap<RepoPath, String> = self
 832            .fs
 833            .files()
 834            .iter()
 835            .filter_map(|path| {
 836                let repo_path = path.strip_prefix(&workdir_path).ok()?;
 837                if repo_path.starts_with(".git") {
 838                    return None;
 839                }
 840                let content = self
 841                    .fs
 842                    .read_file_sync(path)
 843                    .ok()
 844                    .and_then(|bytes| String::from_utf8(bytes).ok())?;
 845                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
 846                Some((RepoPath::from_rel_path(&repo_path), content))
 847            })
 848            .collect();
 849
 850        self.with_state_async(false, move |state| {
 851            let mut entries = Vec::new();
 852            let all_paths: HashSet<&RepoPath> = state
 853                .head_contents
 854                .keys()
 855                .chain(
 856                    worktree_files
 857                        .keys()
 858                        .filter(|p| state.index_contents.contains_key(*p)),
 859                )
 860                .collect();
 861            for path in all_paths {
 862                if !matches_prefixes(path, &path_prefixes) {
 863                    continue;
 864                }
 865                let head = state.head_contents.get(path);
 866                let worktree = worktree_files.get(path);
 867                match (head, worktree) {
 868                    (Some(old), Some(new)) if old != new => {
 869                        entries.push((
 870                            path.clone(),
 871                            git::status::DiffStat {
 872                                added: count_lines(new),
 873                                deleted: count_lines(old),
 874                            },
 875                        ));
 876                    }
 877                    (Some(old), None) => {
 878                        entries.push((
 879                            path.clone(),
 880                            git::status::DiffStat {
 881                                added: 0,
 882                                deleted: count_lines(old),
 883                            },
 884                        ));
 885                    }
 886                    (None, Some(new)) => {
 887                        entries.push((
 888                            path.clone(),
 889                            git::status::DiffStat {
 890                                added: count_lines(new),
 891                                deleted: 0,
 892                            },
 893                        ));
 894                    }
 895                    _ => {}
 896                }
 897            }
 898            entries.sort_by(|(a, _), (b, _)| a.cmp(b));
 899            Ok(git::status::GitDiffStat {
 900                entries: entries.into(),
 901            })
 902        })
 903        .boxed()
 904    }
 905
 906    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
 907        let executor = self.executor.clone();
 908        let fs = self.fs.clone();
 909        let checkpoints = self.checkpoints.clone();
 910        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
 911        async move {
 912            executor.simulate_random_delay().await;
 913            let oid = git::Oid::random(&mut *executor.rng().lock());
 914            let entry = fs.entry(&repository_dir_path)?;
 915            checkpoints.lock().insert(oid, entry);
 916            Ok(GitRepositoryCheckpoint { commit_sha: oid })
 917        }
 918        .boxed()
 919    }
 920
 921    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
 922        let executor = self.executor.clone();
 923        let fs = self.fs.clone();
 924        let checkpoints = self.checkpoints.clone();
 925        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
 926        async move {
 927            executor.simulate_random_delay().await;
 928            let checkpoints = checkpoints.lock();
 929            let entry = checkpoints
 930                .get(&checkpoint.commit_sha)
 931                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
 932            fs.insert_entry(&repository_dir_path, entry.clone())?;
 933            Ok(())
 934        }
 935        .boxed()
 936    }
 937
 938    fn compare_checkpoints(
 939        &self,
 940        left: GitRepositoryCheckpoint,
 941        right: GitRepositoryCheckpoint,
 942    ) -> BoxFuture<'_, Result<bool>> {
 943        let executor = self.executor.clone();
 944        let checkpoints = self.checkpoints.clone();
 945        async move {
 946            executor.simulate_random_delay().await;
 947            let checkpoints = checkpoints.lock();
 948            let left = checkpoints
 949                .get(&left.commit_sha)
 950                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
 951            let right = checkpoints
 952                .get(&right.commit_sha)
 953                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
 954
 955            Ok(left == right)
 956        }
 957        .boxed()
 958    }
 959
 960    fn diff_checkpoints(
 961        &self,
 962        _base_checkpoint: GitRepositoryCheckpoint,
 963        _target_checkpoint: GitRepositoryCheckpoint,
 964    ) -> BoxFuture<'_, Result<String>> {
 965        unimplemented!()
 966    }
 967
 968    fn default_branch(
 969        &self,
 970        include_remote_name: bool,
 971    ) -> BoxFuture<'_, Result<Option<SharedString>>> {
 972        async move {
 973            Ok(Some(if include_remote_name {
 974                "origin/main".into()
 975            } else {
 976                "main".into()
 977            }))
 978        }
 979        .boxed()
 980    }
 981
 982    fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
 983        self.with_state_async(true, move |state| {
 984            state.remotes.insert(name, url);
 985            Ok(())
 986        })
 987    }
 988
 989    fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
 990        self.with_state_async(true, move |state| {
 991            state.branches.retain(|branch| {
 992                branch
 993                    .split_once('/')
 994                    .is_none_or(|(remote, _)| remote != name)
 995            });
 996            state.remotes.remove(&name);
 997            Ok(())
 998        })
 999    }
1000
1001    fn initial_graph_data(
1002        &self,
1003        _log_source: LogSource,
1004        _log_order: LogOrder,
1005        request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
1006    ) -> BoxFuture<'_, Result<()>> {
1007        let fs = self.fs.clone();
1008        let dot_git_path = self.dot_git_path.clone();
1009        async move {
1010            let graph_commits =
1011                fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
1012
1013            for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1014                request_tx.send(chunk.to_vec()).await.ok();
1015            }
1016            Ok(())
1017        }
1018        .boxed()
1019    }
1020
1021    fn commit_data_reader(&self) -> Result<CommitDataReader> {
1022        anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
1023    }
1024
1025    fn set_trusted(&self, trusted: bool) {
1026        self.is_trusted
1027            .store(trusted, std::sync::atomic::Ordering::Release);
1028    }
1029
1030    fn is_trusted(&self) -> bool {
1031        self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1032    }
1033}