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