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 {
 396                        format!("refs/heads/{branch_name}").into()
 397                    };
 398                    Branch {
 399                        is_head: Some(branch_name) == current_branch.as_ref(),
 400                        ref_name,
 401                        most_recent_commit: None,
 402                        upstream: None,
 403                    }
 404                })
 405                .collect())
 406        })
 407    }
 408
 409    fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
 410        let dot_git_path = self.dot_git_path.clone();
 411        self.with_state_async(false, move |state| {
 412            let work_dir = dot_git_path
 413                .parent()
 414                .map(PathBuf::from)
 415                .unwrap_or(dot_git_path);
 416            let head_sha = state
 417                .refs
 418                .get("HEAD")
 419                .cloned()
 420                .unwrap_or_else(|| "0000000".to_string());
 421            let branch_ref = state
 422                .current_branch_name
 423                .as_ref()
 424                .map(|name| format!("refs/heads/{name}"))
 425                .unwrap_or_else(|| "refs/heads/main".to_string());
 426            let main_worktree = Worktree {
 427                path: work_dir,
 428                ref_name: branch_ref.into(),
 429                sha: head_sha.into(),
 430            };
 431            let mut all = vec![main_worktree];
 432            all.extend(state.worktrees.iter().cloned());
 433            Ok(all)
 434        })
 435    }
 436
 437    fn create_worktree(
 438        &self,
 439        name: String,
 440        directory: PathBuf,
 441        from_commit: Option<String>,
 442    ) -> BoxFuture<'_, Result<()>> {
 443        let fs = self.fs.clone();
 444        let executor = self.executor.clone();
 445        let dot_git_path = self.dot_git_path.clone();
 446        async move {
 447            let path = directory.join(&name);
 448            executor.simulate_random_delay().await;
 449            // Check for simulated error before any side effects
 450            fs.with_git_state(&dot_git_path, false, |state| {
 451                if let Some(message) = &state.simulated_create_worktree_error {
 452                    anyhow::bail!("{message}");
 453                }
 454                Ok(())
 455            })??;
 456            // Create directory before updating state so state is never
 457            // inconsistent with the filesystem
 458            fs.create_dir(&path).await?;
 459            fs.with_git_state(&dot_git_path, true, {
 460                let path = path.clone();
 461                move |state| {
 462                    if state.branches.contains(&name) {
 463                        bail!("a branch named '{}' already exists", name);
 464                    }
 465                    let ref_name = format!("refs/heads/{name}");
 466                    let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
 467                    state.refs.insert(ref_name.clone(), sha.clone());
 468                    state.worktrees.push(Worktree {
 469                        path,
 470                        ref_name: ref_name.into(),
 471                        sha: sha.into(),
 472                    });
 473                    state.branches.insert(name);
 474                    Ok::<(), anyhow::Error>(())
 475                }
 476            })??;
 477            Ok(())
 478        }
 479        .boxed()
 480    }
 481
 482    fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
 483        let fs = self.fs.clone();
 484        let executor = self.executor.clone();
 485        let dot_git_path = self.dot_git_path.clone();
 486        async move {
 487            executor.simulate_random_delay().await;
 488            // Validate the worktree exists in state before touching the filesystem
 489            fs.with_git_state(&dot_git_path, false, {
 490                let path = path.clone();
 491                move |state| {
 492                    if !state.worktrees.iter().any(|w| w.path == path) {
 493                        bail!("no worktree found at path: {}", path.display());
 494                    }
 495                    Ok(())
 496                }
 497            })??;
 498            // Now remove the directory
 499            fs.remove_dir(
 500                &path,
 501                RemoveOptions {
 502                    recursive: true,
 503                    ignore_if_not_exists: false,
 504                },
 505            )
 506            .await?;
 507            // Update state
 508            fs.with_git_state(&dot_git_path, true, move |state| {
 509                state.worktrees.retain(|worktree| worktree.path != path);
 510                Ok::<(), anyhow::Error>(())
 511            })??;
 512            Ok(())
 513        }
 514        .boxed()
 515    }
 516
 517    fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
 518        let fs = self.fs.clone();
 519        let executor = self.executor.clone();
 520        let dot_git_path = self.dot_git_path.clone();
 521        async move {
 522            executor.simulate_random_delay().await;
 523            // Validate the worktree exists in state before touching the filesystem
 524            fs.with_git_state(&dot_git_path, false, {
 525                let old_path = old_path.clone();
 526                move |state| {
 527                    if !state.worktrees.iter().any(|w| w.path == old_path) {
 528                        bail!("no worktree found at path: {}", old_path.display());
 529                    }
 530                    Ok(())
 531                }
 532            })??;
 533            // Now move the directory
 534            fs.rename(
 535                &old_path,
 536                &new_path,
 537                RenameOptions {
 538                    overwrite: false,
 539                    ignore_if_exists: false,
 540                    create_parents: true,
 541                },
 542            )
 543            .await?;
 544            // Update state
 545            fs.with_git_state(&dot_git_path, true, move |state| {
 546                let worktree = state
 547                    .worktrees
 548                    .iter_mut()
 549                    .find(|worktree| worktree.path == old_path)
 550                    .expect("worktree was validated above");
 551                worktree.path = new_path;
 552                Ok::<(), anyhow::Error>(())
 553            })??;
 554            Ok(())
 555        }
 556        .boxed()
 557    }
 558
 559    fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
 560        self.with_state_async(true, |state| {
 561            state.current_branch_name = Some(name);
 562            Ok(())
 563        })
 564    }
 565
 566    fn create_branch(
 567        &self,
 568        name: String,
 569        _base_branch: Option<String>,
 570    ) -> BoxFuture<'_, Result<()>> {
 571        self.with_state_async(true, move |state| {
 572            state.branches.insert(name);
 573            Ok(())
 574        })
 575    }
 576
 577    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
 578        self.with_state_async(true, move |state| {
 579            if !state.branches.remove(&branch) {
 580                bail!("no such branch: {branch}");
 581            }
 582            state.branches.insert(new_name.clone());
 583            if state.current_branch_name == Some(branch) {
 584                state.current_branch_name = Some(new_name);
 585            }
 586            Ok(())
 587        })
 588    }
 589
 590    fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
 591        self.with_state_async(true, move |state| {
 592            if !state.branches.remove(&name) {
 593                bail!("no such branch: {name}");
 594            }
 595            Ok(())
 596        })
 597    }
 598
 599    fn blame(
 600        &self,
 601        path: RepoPath,
 602        _content: Rope,
 603        _line_ending: LineEnding,
 604    ) -> BoxFuture<'_, Result<git::blame::Blame>> {
 605        self.with_state_async(false, move |state| {
 606            state
 607                .blames
 608                .get(&path)
 609                .with_context(|| format!("failed to get blame for {:?}", path))
 610                .cloned()
 611        })
 612    }
 613
 614    fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
 615        self.file_history_paginated(path, 0, None)
 616    }
 617
 618    fn file_history_paginated(
 619        &self,
 620        path: RepoPath,
 621        _skip: usize,
 622        _limit: Option<usize>,
 623    ) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
 624        async move {
 625            Ok(git::repository::FileHistory {
 626                entries: Vec::new(),
 627                path,
 628            })
 629        }
 630        .boxed()
 631    }
 632
 633    fn stage_paths(
 634        &self,
 635        paths: Vec<RepoPath>,
 636        _env: Arc<HashMap<String, String>>,
 637    ) -> BoxFuture<'_, Result<()>> {
 638        Box::pin(async move {
 639            let contents = paths
 640                .into_iter()
 641                .map(|path| {
 642                    let abs_path = self
 643                        .dot_git_path
 644                        .parent()
 645                        .unwrap()
 646                        .join(&path.as_std_path());
 647                    Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
 648                })
 649                .collect::<Vec<_>>();
 650            let contents = join_all(contents).await;
 651            self.with_state_async(true, move |state| {
 652                for (path, content) in contents {
 653                    if let Some(content) = content {
 654                        state.index_contents.insert(path, content);
 655                    } else {
 656                        state.index_contents.remove(&path);
 657                    }
 658                }
 659                Ok(())
 660            })
 661            .await
 662        })
 663    }
 664
 665    fn unstage_paths(
 666        &self,
 667        paths: Vec<RepoPath>,
 668        _env: Arc<HashMap<String, String>>,
 669    ) -> BoxFuture<'_, Result<()>> {
 670        self.with_state_async(true, move |state| {
 671            for path in paths {
 672                match state.head_contents.get(&path) {
 673                    Some(content) => state.index_contents.insert(path, content.clone()),
 674                    None => state.index_contents.remove(&path),
 675                };
 676            }
 677            Ok(())
 678        })
 679    }
 680
 681    fn stash_paths(
 682        &self,
 683        _paths: Vec<RepoPath>,
 684        _env: Arc<HashMap<String, String>>,
 685    ) -> BoxFuture<'_, Result<()>> {
 686        unimplemented!()
 687    }
 688
 689    fn stash_pop(
 690        &self,
 691        _index: Option<usize>,
 692        _env: Arc<HashMap<String, String>>,
 693    ) -> BoxFuture<'_, Result<()>> {
 694        unimplemented!()
 695    }
 696
 697    fn stash_apply(
 698        &self,
 699        _index: Option<usize>,
 700        _env: Arc<HashMap<String, String>>,
 701    ) -> BoxFuture<'_, Result<()>> {
 702        unimplemented!()
 703    }
 704
 705    fn stash_drop(
 706        &self,
 707        _index: Option<usize>,
 708        _env: Arc<HashMap<String, String>>,
 709    ) -> BoxFuture<'_, Result<()>> {
 710        unimplemented!()
 711    }
 712
 713    fn commit(
 714        &self,
 715        _message: gpui::SharedString,
 716        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
 717        _options: CommitOptions,
 718        _askpass: AskPassDelegate,
 719        _env: Arc<HashMap<String, String>>,
 720    ) -> BoxFuture<'_, Result<()>> {
 721        async { Ok(()) }.boxed()
 722    }
 723
 724    fn run_hook(
 725        &self,
 726        _hook: RunHook,
 727        _env: Arc<HashMap<String, String>>,
 728    ) -> BoxFuture<'_, Result<()>> {
 729        async { Ok(()) }.boxed()
 730    }
 731
 732    fn push(
 733        &self,
 734        _branch: String,
 735        _remote_branch: String,
 736        _remote: String,
 737        _options: Option<PushOptions>,
 738        _askpass: AskPassDelegate,
 739        _env: Arc<HashMap<String, String>>,
 740        _cx: AsyncApp,
 741    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 742        unimplemented!()
 743    }
 744
 745    fn pull(
 746        &self,
 747        _branch: Option<String>,
 748        _remote: String,
 749        _rebase: bool,
 750        _askpass: AskPassDelegate,
 751        _env: Arc<HashMap<String, String>>,
 752        _cx: AsyncApp,
 753    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 754        unimplemented!()
 755    }
 756
 757    fn fetch(
 758        &self,
 759        _fetch_options: FetchOptions,
 760        _askpass: AskPassDelegate,
 761        _env: Arc<HashMap<String, String>>,
 762        _cx: AsyncApp,
 763    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
 764        unimplemented!()
 765    }
 766
 767    fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
 768        self.with_state_async(false, move |state| {
 769            let remotes = state
 770                .remotes
 771                .keys()
 772                .map(|r| Remote {
 773                    name: r.clone().into(),
 774                })
 775                .collect::<Vec<_>>();
 776            Ok(remotes)
 777        })
 778    }
 779
 780    fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
 781        unimplemented!()
 782    }
 783
 784    fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
 785        unimplemented!()
 786    }
 787
 788    fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
 789        future::ready(Ok(Vec::new())).boxed()
 790    }
 791
 792    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
 793        future::ready(Ok(String::new())).boxed()
 794    }
 795
 796    fn diff_stat(
 797        &self,
 798        path_prefixes: &[RepoPath],
 799    ) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
 800        fn count_lines(s: &str) -> u32 {
 801            if s.is_empty() {
 802                0
 803            } else {
 804                s.lines().count() as u32
 805            }
 806        }
 807
 808        fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
 809            if prefixes.is_empty() {
 810                return true;
 811            }
 812            prefixes.iter().any(|prefix| {
 813                let prefix_str = prefix.as_unix_str();
 814                if prefix_str == "." {
 815                    return true;
 816                }
 817                path == prefix || path.starts_with(&prefix)
 818            })
 819        }
 820
 821        let path_prefixes = path_prefixes.to_vec();
 822
 823        let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
 824        let worktree_files: HashMap<RepoPath, String> = self
 825            .fs
 826            .files()
 827            .iter()
 828            .filter_map(|path| {
 829                let repo_path = path.strip_prefix(&workdir_path).ok()?;
 830                if repo_path.starts_with(".git") {
 831                    return None;
 832                }
 833                let content = self
 834                    .fs
 835                    .read_file_sync(path)
 836                    .ok()
 837                    .and_then(|bytes| String::from_utf8(bytes).ok())?;
 838                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
 839                Some((RepoPath::from_rel_path(&repo_path), content))
 840            })
 841            .collect();
 842
 843        self.with_state_async(false, move |state| {
 844            let mut entries = Vec::new();
 845            let all_paths: HashSet<&RepoPath> = state
 846                .head_contents
 847                .keys()
 848                .chain(
 849                    worktree_files
 850                        .keys()
 851                        .filter(|p| state.index_contents.contains_key(*p)),
 852                )
 853                .collect();
 854            for path in all_paths {
 855                if !matches_prefixes(path, &path_prefixes) {
 856                    continue;
 857                }
 858                let head = state.head_contents.get(path);
 859                let worktree = worktree_files.get(path);
 860                match (head, worktree) {
 861                    (Some(old), Some(new)) if old != new => {
 862                        entries.push((
 863                            path.clone(),
 864                            git::status::DiffStat {
 865                                added: count_lines(new),
 866                                deleted: count_lines(old),
 867                            },
 868                        ));
 869                    }
 870                    (Some(old), None) => {
 871                        entries.push((
 872                            path.clone(),
 873                            git::status::DiffStat {
 874                                added: 0,
 875                                deleted: count_lines(old),
 876                            },
 877                        ));
 878                    }
 879                    (None, Some(new)) => {
 880                        entries.push((
 881                            path.clone(),
 882                            git::status::DiffStat {
 883                                added: count_lines(new),
 884                                deleted: 0,
 885                            },
 886                        ));
 887                    }
 888                    _ => {}
 889                }
 890            }
 891            entries.sort_by(|(a, _), (b, _)| a.cmp(b));
 892            Ok(git::status::GitDiffStat {
 893                entries: entries.into(),
 894            })
 895        })
 896        .boxed()
 897    }
 898
 899    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
 900        let executor = self.executor.clone();
 901        let fs = self.fs.clone();
 902        let checkpoints = self.checkpoints.clone();
 903        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
 904        async move {
 905            executor.simulate_random_delay().await;
 906            let oid = git::Oid::random(&mut *executor.rng().lock());
 907            let entry = fs.entry(&repository_dir_path)?;
 908            checkpoints.lock().insert(oid, entry);
 909            Ok(GitRepositoryCheckpoint { commit_sha: oid })
 910        }
 911        .boxed()
 912    }
 913
 914    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
 915        let executor = self.executor.clone();
 916        let fs = self.fs.clone();
 917        let checkpoints = self.checkpoints.clone();
 918        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
 919        async move {
 920            executor.simulate_random_delay().await;
 921            let checkpoints = checkpoints.lock();
 922            let entry = checkpoints
 923                .get(&checkpoint.commit_sha)
 924                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
 925            fs.insert_entry(&repository_dir_path, entry.clone())?;
 926            Ok(())
 927        }
 928        .boxed()
 929    }
 930
 931    fn compare_checkpoints(
 932        &self,
 933        left: GitRepositoryCheckpoint,
 934        right: GitRepositoryCheckpoint,
 935    ) -> BoxFuture<'_, Result<bool>> {
 936        let executor = self.executor.clone();
 937        let checkpoints = self.checkpoints.clone();
 938        async move {
 939            executor.simulate_random_delay().await;
 940            let checkpoints = checkpoints.lock();
 941            let left = checkpoints
 942                .get(&left.commit_sha)
 943                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
 944            let right = checkpoints
 945                .get(&right.commit_sha)
 946                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
 947
 948            Ok(left == right)
 949        }
 950        .boxed()
 951    }
 952
 953    fn diff_checkpoints(
 954        &self,
 955        _base_checkpoint: GitRepositoryCheckpoint,
 956        _target_checkpoint: GitRepositoryCheckpoint,
 957    ) -> BoxFuture<'_, Result<String>> {
 958        unimplemented!()
 959    }
 960
 961    fn default_branch(
 962        &self,
 963        include_remote_name: bool,
 964    ) -> BoxFuture<'_, Result<Option<SharedString>>> {
 965        async move {
 966            Ok(Some(if include_remote_name {
 967                "origin/main".into()
 968            } else {
 969                "main".into()
 970            }))
 971        }
 972        .boxed()
 973    }
 974
 975    fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
 976        self.with_state_async(true, move |state| {
 977            state.remotes.insert(name, url);
 978            Ok(())
 979        })
 980    }
 981
 982    fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
 983        self.with_state_async(true, move |state| {
 984            state.remotes.remove(&name);
 985            Ok(())
 986        })
 987    }
 988
 989    fn initial_graph_data(
 990        &self,
 991        _log_source: LogSource,
 992        _log_order: LogOrder,
 993        request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
 994    ) -> BoxFuture<'_, Result<()>> {
 995        let fs = self.fs.clone();
 996        let dot_git_path = self.dot_git_path.clone();
 997        async move {
 998            let graph_commits =
 999                fs.with_git_state(&dot_git_path, false, |state| state.graph_commits.clone())?;
1000
1001            for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1002                request_tx.send(chunk.to_vec()).await.ok();
1003            }
1004            Ok(())
1005        }
1006        .boxed()
1007    }
1008
1009    fn commit_data_reader(&self) -> Result<CommitDataReader> {
1010        anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
1011    }
1012
1013    fn set_trusted(&self, trusted: bool) {
1014        self.is_trusted
1015            .store(trusted, std::sync::atomic::Ordering::Release);
1016    }
1017
1018    fn is_trusted(&self) -> bool {
1019        self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1020    }
1021}