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