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