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