Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:54:38.118Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

fake_git_repo.rs

1620 lines · 60.1 KB · rust
1use std::path::Path;
2
3use crate::{FakeFs, FakeFsEntry, Fs, RemoveOptions, RenameOptions};
4use anyhow::{Context as _, Result, bail};
5use async_channel::Sender;
6use collections::{HashMap, HashSet};
7use futures::FutureExt as _;
8use futures::future::{self, BoxFuture, join_all};
9use git::repository::GitCommitTemplate;
10use git::{
11    Oid, RunHook,
12    blame::Blame,
13    repository::{
14        AskPassDelegate, Branch, CommitData, CommitDataReader, CommitDetails, CommitOptions,
15        CreateWorktreeTarget, FetchOptions, FileHistoryChangedFileSets, GRAPH_CHUNK_SIZE,
16        GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder, LogSource,
17        PushOptions, RefEdit, Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree,
18        commit_hash_search_query,
19    },
20    stash::GitStash,
21    status::{
22        DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
23        UnmergedStatus,
24    },
25};
26use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
27use ignore::gitignore::GitignoreBuilder;
28use parking_lot::Mutex;
29use rope::Rope;
30use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool, time::SystemTime};
31use text::LineEnding;
32use util::{paths::PathStyle, rel_path::RelPath};
33
34#[derive(Clone)]
35pub struct FakeGitRepository {
36    pub(crate) fs: Arc<FakeFs>,
37    pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
38    pub(crate) executor: BackgroundExecutor,
39    pub(crate) dot_git_path: PathBuf,
40    pub(crate) repository_dir_path: PathBuf,
41    pub(crate) common_dir_path: PathBuf,
42    pub(crate) is_trusted: Arc<AtomicBool>,
43}
44
45#[derive(Debug, Clone)]
46pub struct FakeCommitSnapshot {
47    pub head_contents: HashMap<RepoPath, String>,
48    pub index_contents: HashMap<RepoPath, String>,
49    pub sha: String,
50}
51
52#[derive(Debug, Clone)]
53pub enum FakeCommitDataEntry {
54    Success(CommitData),
55    Fail(CommitData),
56}
57
58#[derive(Debug, Clone)]
59pub struct FakeGitRepositoryState {
60    pub commit_history: Vec<FakeCommitSnapshot>,
61    pub event_emitter: async_channel::Sender<PathBuf>,
62    pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
63    pub head_contents: HashMap<RepoPath, String>,
64    pub index_contents: HashMap<RepoPath, String>,
65    // everything in commit contents is in oids
66    pub merge_base_contents: HashMap<RepoPath, Oid>,
67    pub oids: HashMap<Oid, String>,
68    pub blames: HashMap<RepoPath, Blame>,
69    pub current_branch_name: Option<String>,
70    pub branches: HashSet<String>,
71    /// List of remotes, keys are names and values are URLs
72    pub remotes: HashMap<String, String>,
73    pub simulated_index_write_error_message: Option<String>,
74    pub simulated_create_worktree_error: Option<String>,
75    pub simulated_graph_error: Option<String>,
76    pub branches_requiring_force_delete: HashSet<String>,
77    pub worktrees_requiring_force_delete: HashSet<PathBuf>,
78    pub refs: HashMap<String, String>,
79    pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
80    pub commit_data: HashMap<Oid, FakeCommitDataEntry>,
81    pub stash_entries: GitStash,
82    pub commit_template: Option<GitCommitTemplate>,
83}
84
85impl FakeGitRepositoryState {
86    pub fn new(event_emitter: async_channel::Sender<PathBuf>) -> Self {
87        FakeGitRepositoryState {
88            event_emitter,
89            head_contents: Default::default(),
90            index_contents: Default::default(),
91            unmerged_paths: Default::default(),
92            blames: Default::default(),
93            current_branch_name: Default::default(),
94            branches: Default::default(),
95            simulated_index_write_error_message: Default::default(),
96            simulated_create_worktree_error: Default::default(),
97            simulated_graph_error: None,
98            branches_requiring_force_delete: Default::default(),
99            worktrees_requiring_force_delete: Default::default(),
100            refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
101            merge_base_contents: Default::default(),
102            oids: Default::default(),
103            remotes: HashMap::default(),
104            graph_commits: Vec::new(),
105            commit_data: Default::default(),
106            commit_history: Vec::new(),
107            stash_entries: Default::default(),
108            commit_template: None,
109        }
110    }
111}
112
113impl FakeGitRepository {
114    fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
115    where
116        F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
117        T: Send,
118    {
119        let fs = self.fs.clone();
120        let executor = self.executor.clone();
121        let dot_git_path = self.dot_git_path.clone();
122        async move {
123            executor.simulate_random_delay().await;
124            fs.with_git_state(&dot_git_path, write, f)?
125        }
126        .boxed()
127    }
128
129    fn edit_ref(&self, edit: RefEdit) -> BoxFuture<'_, Result<()>> {
130        self.with_state_async(true, move |state| {
131            match edit {
132                RefEdit::Update { ref_name, commit } => {
133                    state.refs.insert(ref_name, commit);
134                }
135                RefEdit::Delete { ref_name } => {
136                    state.refs.remove(&ref_name);
137                }
138            }
139            Ok(())
140        })
141    }
142
143    /// Scans `.git/worktrees/*/gitdir` to find the admin entry directory for a
144    /// worktree at the given checkout path. Used when the working tree directory
145    /// has already been deleted and we can't read its `.git` pointer file.
146    async fn find_worktree_entry_dir_by_path(&self, path: &Path) -> Option<PathBuf> {
147        use futures::StreamExt;
148
149        let worktrees_dir = self.common_dir_path.join("worktrees");
150        let mut entries = self.fs.read_dir(&worktrees_dir).await.ok()?;
151        while let Some(Ok(entry_path)) = entries.next().await {
152            if let Ok(gitdir_content) = self.fs.load(&entry_path.join("gitdir")).await {
153                let worktree_path = PathBuf::from(gitdir_content.trim())
154                    .parent()
155                    .map(PathBuf::from)
156                    .unwrap_or_default();
157                if worktree_path == path {
158                    return Some(entry_path);
159                }
160            }
161        }
162        None
163    }
164}
165
166impl GitRepository for FakeGitRepository {
167    fn load_commit_template(&self) -> BoxFuture<'_, Result<Option<GitCommitTemplate>>> {
168        self.with_state_async(false, |state| Ok(state.commit_template.clone()))
169    }
170
171    fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
172        self.with_state_async(false, move |state| {
173            state.oids.get(&oid).cloned().context("oid does not exist")
174        })
175        .boxed()
176    }
177
178    fn load_commit(
179        &self,
180        _commit: String,
181        _cx: AsyncApp,
182    ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
183        async { Ok(git::repository::CommitDiff { files: Vec::new() }) }.boxed()
184    }
185
186    fn set_index_text(
187        &self,
188        path: RepoPath,
189        content: Option<String>,
190        _env: Arc<HashMap<String, String>>,
191        _is_executable: bool,
192    ) -> BoxFuture<'_, anyhow::Result<()>> {
193        self.with_state_async(true, move |state| {
194            if let Some(message) = &state.simulated_index_write_error_message {
195                anyhow::bail!("{message}");
196            } else if let Some(content) = content {
197                state.index_contents.insert(path, content);
198            } else {
199                state.index_contents.remove(&path);
200            }
201            Ok(())
202        })
203    }
204
205    fn remote_urls(&self) -> BoxFuture<'_, HashMap<String, String>> {
206        let fut = self.with_state_async(false, |state| Ok(state.remotes.clone()));
207        async move { fut.await.unwrap_or_default() }.boxed()
208    }
209
210    fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
211        let mut entries = HashMap::default();
212        self.with_state_async(false, |state| {
213            for (path, content) in &state.head_contents {
214                let status = if let Some((oid, original)) = state
215                    .merge_base_contents
216                    .get(path)
217                    .map(|oid| (oid, &state.oids[oid]))
218                {
219                    if original == content {
220                        continue;
221                    }
222                    TreeDiffStatus::Modified { old: *oid }
223                } else {
224                    TreeDiffStatus::Added
225                };
226                entries.insert(path.clone(), status);
227            }
228            for (path, oid) in &state.merge_base_contents {
229                if !entries.contains_key(path) {
230                    entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
231                }
232            }
233            Ok(TreeDiff { entries })
234        })
235        .boxed()
236    }
237
238    fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
239        self.with_state_async(false, |state| {
240            Ok(revs
241                .into_iter()
242                .map(|rev| state.refs.get(&rev).cloned())
243                .collect())
244        })
245    }
246
247    fn load_revisions(&self, revisions: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
248        let fut = self.with_state_async(false, move |state| {
249            Ok(revisions
250                .into_iter()
251                .map(|rev| {
252                    let (prefix, path) = rev.split_once(':')?;
253                    let repo_path = RepoPath::new(path).ok()?;
254                    match prefix {
255                        "" => state.index_contents.get(&repo_path).cloned(),
256                        "HEAD" => state.head_contents.get(&repo_path).cloned(),
257                        _ => None,
258                    }
259                })
260                .collect())
261        });
262        self.executor.spawn(fut).boxed()
263    }
264
265    fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
266        self.with_state_async(false, move |state| {
267            let sha = match state.refs.get(&commit) {
268                Some(sha) => sha.clone(),
269                // Real git fails to show an unresolvable revision (e.g. HEAD on an
270                // unborn branch), so only fall back to treating the input as a sha.
271                None => {
272                    anyhow::ensure!(
273                        commit.parse::<Oid>().is_ok(),
274                        "unable to resolve revision: {commit}"
275                    );
276                    commit
277                }
278            };
279            Ok(CommitDetails {
280                sha: sha.into(),
281                message: "initial commit".into(),
282                ..Default::default()
283            })
284        })
285    }
286
287    fn reset(
288        &self,
289        commit: String,
290        mode: ResetMode,
291        _env: Arc<HashMap<String, String>>,
292    ) -> BoxFuture<'_, Result<()>> {
293        self.with_state_async(true, move |state| {
294            let pop_count = if commit == "HEAD~" || commit == "HEAD^" {
295                1
296            } else if let Some(suffix) = commit.strip_prefix("HEAD~") {
297                suffix
298                    .parse::<usize>()
299                    .with_context(|| format!("Invalid HEAD~ offset: {commit}"))?
300            } else {
301                match state
302                    .commit_history
303                    .iter()
304                    .rposition(|entry| entry.sha == commit)
305                {
306                    Some(index) => state.commit_history.len() - index,
307                    None => anyhow::bail!("Unknown commit ref: {commit}"),
308                }
309            };
310
311            if pop_count == 0 || pop_count > state.commit_history.len() {
312                anyhow::bail!(
313                    "Cannot reset {pop_count} commit(s): only {} in history",
314                    state.commit_history.len()
315                );
316            }
317
318            let target_index = state.commit_history.len() - pop_count;
319            let snapshot = state.commit_history[target_index].clone();
320            state.commit_history.truncate(target_index);
321
322            match mode {
323                ResetMode::Soft => {
324                    state.head_contents = snapshot.head_contents;
325                }
326                ResetMode::Mixed => {
327                    state.head_contents = snapshot.head_contents;
328                    state.index_contents = state.head_contents.clone();
329                }
330            }
331
332            state.refs.insert("HEAD".into(), snapshot.sha);
333            Ok(())
334        })
335    }
336
337    fn checkout_files(
338        &self,
339        _commit: String,
340        _paths: Vec<RepoPath>,
341        _env: Arc<HashMap<String, String>>,
342    ) -> BoxFuture<'_, Result<()>> {
343        unimplemented!()
344    }
345
346    fn path(&self) -> PathBuf {
347        self.repository_dir_path.clone()
348    }
349
350    fn main_repository_path(&self) -> PathBuf {
351        self.common_dir_path.clone()
352    }
353
354    fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
355        async move { None }.boxed()
356    }
357
358    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
359        let workdir_path = self.dot_git_path.parent().unwrap();
360
361        // Load gitignores
362        let ignores = workdir_path
363            .ancestors()
364            .filter_map(|dir| {
365                let ignore_path = dir.join(".gitignore");
366                let content = self.fs.read_file_sync(ignore_path).ok()?;
367                let content = String::from_utf8(content).ok()?;
368                let mut builder = GitignoreBuilder::new(dir);
369                for line in content.lines() {
370                    builder.add_line(Some(dir.into()), line).ok()?;
371                }
372                builder.build().ok()
373            })
374            .collect::<Vec<_>>();
375
376        // Load working copy files.
377        let git_files: HashMap<RepoPath, (String, bool)> = self
378            .fs
379            .files()
380            .iter()
381            .filter_map(|path| {
382                // TODO better simulate git status output in the case of submodules and worktrees
383                let repo_path = path.strip_prefix(workdir_path).ok()?;
384                let mut is_ignored = repo_path.starts_with(".git");
385                for ignore in &ignores {
386                    match ignore.matched_path_or_any_parents(path, false) {
387                        ignore::Match::None => {}
388                        ignore::Match::Ignore(_) => is_ignored = true,
389                        ignore::Match::Whitelist(_) => break,
390                    }
391                }
392                let content = self
393                    .fs
394                    .read_file_sync(path)
395                    .ok()
396                    .map(|content| String::from_utf8(content).unwrap())?;
397                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
398                Some((RepoPath::from_rel_path(&repo_path), (content, is_ignored)))
399            })
400            .collect();
401
402        let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
403            let mut entries = Vec::new();
404            let paths = state
405                .head_contents
406                .keys()
407                .chain(state.index_contents.keys())
408                .chain(git_files.keys())
409                .collect::<HashSet<_>>();
410            for path in paths {
411                if !path_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
412                    continue;
413                }
414
415                let head = state.head_contents.get(path);
416                let index = state.index_contents.get(path);
417                let unmerged = state.unmerged_paths.get(path);
418                let fs = git_files.get(path);
419                let status = match (unmerged, head, index, fs) {
420                    (Some(unmerged), _, _, _) => FileStatus::Unmerged(*unmerged),
421                    (_, Some(head), Some(index), Some((fs, _))) => {
422                        FileStatus::Tracked(TrackedStatus {
423                            index_status: if head == index {
424                                StatusCode::Unmodified
425                            } else {
426                                StatusCode::Modified
427                            },
428                            worktree_status: if fs == index {
429                                StatusCode::Unmodified
430                            } else {
431                                StatusCode::Modified
432                            },
433                        })
434                    }
435                    (_, Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
436                        index_status: if head == index {
437                            StatusCode::Unmodified
438                        } else {
439                            StatusCode::Modified
440                        },
441                        worktree_status: StatusCode::Deleted,
442                    }),
443                    (_, Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
444                        index_status: StatusCode::Deleted,
445                        worktree_status: StatusCode::Added,
446                    }),
447                    (_, Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
448                        index_status: StatusCode::Deleted,
449                        worktree_status: StatusCode::Deleted,
450                    }),
451                    (_, None, Some(index), Some((fs, _))) => FileStatus::Tracked(TrackedStatus {
452                        index_status: StatusCode::Added,
453                        worktree_status: if fs == index {
454                            StatusCode::Unmodified
455                        } else {
456                            StatusCode::Modified
457                        },
458                    }),
459                    (_, None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
460                        index_status: StatusCode::Added,
461                        worktree_status: StatusCode::Deleted,
462                    }),
463                    (_, None, None, Some((_, is_ignored))) => {
464                        if *is_ignored {
465                            continue;
466                        }
467                        FileStatus::Untracked
468                    }
469                    (_, None, None, None) => {
470                        unreachable!();
471                    }
472                };
473                if status
474                    != FileStatus::Tracked(TrackedStatus {
475                        index_status: StatusCode::Unmodified,
476                        worktree_status: StatusCode::Unmodified,
477                    })
478                {
479                    entries.push((path.clone(), status));
480                }
481            }
482            entries.sort_by(|a, b| a.0.cmp(&b.0));
483            anyhow::Ok(GitStatus {
484                entries: entries.into(),
485            })
486        });
487        Task::ready(match result {
488            Ok(result) => result,
489            Err(e) => Err(e),
490        })
491    }
492
493    fn stash_entries(&self) -> BoxFuture<'static, Result<git::stash::GitStash>> {
494        self.with_state_async(false, |state| Ok(state.stash_entries.clone()))
495    }
496
497    fn branches(&self) -> BoxFuture<'_, Result<git::repository::BranchesScanResult>> {
498        self.with_state_async(false, move |state| {
499            let current_branch = &state.current_branch_name;
500            let mut branches = state
501                .branches
502                .iter()
503                .map(|branch_name| {
504                    let ref_name = if branch_name.starts_with("refs/") {
505                        branch_name.into()
506                    } else if branch_name.contains('/') {
507                        format!("refs/remotes/{branch_name}").into()
508                    } else {
509                        format!("refs/heads/{branch_name}").into()
510                    };
511                    Branch {
512                        is_head: Some(branch_name) == current_branch.as_ref(),
513                        ref_name,
514                        most_recent_commit: None,
515                        upstream: None,
516                    }
517                })
518                .collect::<Vec<_>>();
519            // compute snapshot expects these to be sorted by ref_name
520            // because that's what git itself does
521            branches.sort_by(|a, b| a.ref_name.cmp(&b.ref_name));
522            Ok(branches.into())
523        })
524    }
525
526    fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
527        let fs = self.fs.clone();
528        let common_dir_path = self.common_dir_path.clone();
529        let executor = self.executor.clone();
530
531        async move {
532            executor.simulate_random_delay().await;
533
534            let (main_worktree, refs) = fs.with_git_state(&common_dir_path, false, |state| {
535                let work_dir = common_dir_path
536                    .parent()
537                    .map(PathBuf::from)
538                    .unwrap_or_else(|| common_dir_path.clone());
539                let head_sha = state
540                    .refs
541                    .get("HEAD")
542                    .cloned()
543                    .unwrap_or_else(|| "0000000".to_string());
544                let branch_ref = state
545                    .current_branch_name
546                    .as_ref()
547                    .map(|name| format!("refs/heads/{name}"))
548                    .unwrap_or_else(|| "refs/heads/main".to_string());
549                let main_wt = Worktree {
550                    path: work_dir,
551                    ref_name: Some(branch_ref.into()),
552                    sha: head_sha.into(),
553                    is_main: true,
554                    is_bare: false,
555                };
556                (main_wt, state.refs.clone())
557            })?;
558
559            let mut all = vec![main_worktree];
560
561            let worktrees_dir = common_dir_path.join("worktrees");
562            if let Ok(mut entries) = fs.read_dir(&worktrees_dir).await {
563                use futures::StreamExt;
564                while let Some(Ok(entry_path)) = entries.next().await {
565                    let head_content = match fs.load(&entry_path.join("HEAD")).await {
566                        Ok(content) => content,
567                        Err(_) => continue,
568                    };
569                    let gitdir_content = match fs.load(&entry_path.join("gitdir")).await {
570                        Ok(content) => content,
571                        Err(_) => continue,
572                    };
573
574                    let ref_name = head_content
575                        .strip_prefix("ref: ")
576                        .map(|s| s.trim().to_string());
577                    let sha = ref_name
578                        .as_ref()
579                        .and_then(|r| refs.get(r))
580                        .cloned()
581                        .unwrap_or_else(|| head_content.trim().to_string());
582
583                    let worktree_path = PathBuf::from(gitdir_content.trim())
584                        .parent()
585                        .map(PathBuf::from)
586                        .unwrap_or_default();
587
588                    all.push(Worktree {
589                        path: worktree_path,
590                        ref_name: ref_name.map(Into::into),
591                        sha: sha.into(),
592                        is_main: false,
593                        is_bare: false,
594                    });
595                }
596            }
597
598            Ok(all)
599        }
600        .boxed()
601    }
602
603    fn worktree_created_at(
604        &self,
605        worktree_path: PathBuf,
606    ) -> BoxFuture<'_, Result<Option<SystemTime>>> {
607        let fs = self.fs.clone();
608        async move {
609            if fs.metadata(&worktree_path).await?.is_none() {
610                return Ok(None);
611            }
612            let dot_git_path = worktree_path.join(".git");
613            let git_file = fs
614                .load(&dot_git_path)
615                .await
616                .with_context(|| format!("failed to read {}", dot_git_path.display()))?;
617            let git_dir = git_file
618                .strip_prefix("gitdir:")
619                .context("worktree .git file missing gitdir pointer")?
620                .trim();
621            let git_dir = worktree_path.join(git_dir);
622            let metadata = fs
623                .metadata(&git_dir)
624                .await?
625                .with_context(|| format!("missing worktree git dir {}", git_dir.display()))?;
626            // FakeFs assigns directories a monotonically increasing mtime at
627            // creation and never updates it afterwards, so a directory's
628            // mtime doubles as its creation time.
629            Ok(Some(metadata.mtime.0))
630        }
631        .boxed()
632    }
633
634    fn create_worktree(
635        &self,
636        target: CreateWorktreeTarget,
637        path: PathBuf,
638    ) -> BoxFuture<'_, Result<()>> {
639        let fs = self.fs.clone();
640        let executor = self.executor.clone();
641        let dot_git_path = self.dot_git_path.clone();
642        let common_dir_path = self.common_dir_path.clone();
643        async move {
644            executor.simulate_random_delay().await;
645
646            let branch_name = target.branch_name().map(ToOwned::to_owned);
647            let create_branch_ref = matches!(target, CreateWorktreeTarget::NewBranch { .. });
648
649            // Check for simulated error and validate branch state before any side effects.
650            fs.with_git_state(&dot_git_path, false, {
651                let branch_name = branch_name.clone();
652                move |state| {
653                    if let Some(message) = &state.simulated_create_worktree_error {
654                        anyhow::bail!("{message}");
655                    }
656
657                    match (create_branch_ref, branch_name.as_ref()) {
658                        (true, Some(branch_name)) => {
659                            if state.branches.contains(branch_name) {
660                                bail!("a branch named '{}' already exists", branch_name);
661                            }
662                        }
663                        (false, Some(branch_name)) => {
664                            if !state.branches.contains(branch_name) {
665                                bail!("no branch named '{}' exists", branch_name);
666                            }
667                        }
668                        (false, None) => {}
669                        (true, None) => bail!("branch name is required to create a branch"),
670                    }
671
672                    Ok(())
673                }
674            })??;
675
676            let (branch_name, sha, create_branch_ref) = match target {
677                CreateWorktreeTarget::ExistingBranch { branch_name } => {
678                    let ref_name = format!("refs/heads/{branch_name}");
679                    let sha = fs.with_git_state(&dot_git_path, false, {
680                        move |state| {
681                            Ok::<_, anyhow::Error>(
682                                state
683                                    .refs
684                                    .get(&ref_name)
685                                    .cloned()
686                                    .unwrap_or_else(|| "fake-sha".to_string()),
687                            )
688                        }
689                    })??;
690                    (Some(branch_name), sha, false)
691                }
692                CreateWorktreeTarget::NewBranch {
693                    branch_name,
694                    base_sha: start_point,
695                } => (
696                    Some(branch_name),
697                    start_point.unwrap_or_else(|| "fake-sha".to_string()),
698                    true,
699                ),
700                CreateWorktreeTarget::Detached {
701                    base_sha: start_point,
702                } => (
703                    None,
704                    start_point.unwrap_or_else(|| "fake-sha".to_string()),
705                    false,
706                ),
707            };
708
709            // Create the worktree checkout directory.
710            fs.create_dir(&path).await?;
711
712            // Create .git/worktrees/<name>/ directory with HEAD, commondir, gitdir.
713            let worktree_entry_name = branch_name.as_deref().unwrap_or_else(|| {
714                path.file_name()
715                    .and_then(|name| name.to_str())
716                    .unwrap_or("detached")
717            });
718            let worktrees_entry_dir = common_dir_path.join("worktrees").join(worktree_entry_name);
719            fs.create_dir(&worktrees_entry_dir).await?;
720
721            let head_content = if let Some(ref branch_name) = branch_name {
722                let ref_name = format!("refs/heads/{branch_name}");
723                format!("ref: {ref_name}")
724            } else {
725                sha.clone()
726            };
727            fs.write_file_internal(
728                worktrees_entry_dir.join("HEAD"),
729                head_content.into_bytes(),
730                false,
731            )?;
732            fs.write_file_internal(
733                worktrees_entry_dir.join("commondir"),
734                common_dir_path.to_string_lossy().into_owned().into_bytes(),
735                false,
736            )?;
737            let worktree_dot_git = path.join(".git");
738            fs.write_file_internal(
739                worktrees_entry_dir.join("gitdir"),
740                worktree_dot_git.to_string_lossy().into_owned().into_bytes(),
741                false,
742            )?;
743
744            // Create .git file in the worktree checkout.
745            fs.write_file_internal(
746                &worktree_dot_git,
747                format!("gitdir: {}", worktrees_entry_dir.display()).into_bytes(),
748                false,
749            )?;
750
751            // Update git state for newly created branches.
752            if create_branch_ref {
753                fs.with_git_state(&dot_git_path, true, {
754                    let branch_name = branch_name.clone();
755                    let sha = sha.clone();
756                    move |state| {
757                        if let Some(branch_name) = branch_name {
758                            let ref_name = format!("refs/heads/{branch_name}");
759                            state.refs.insert(ref_name, sha);
760                            state.branches.insert(branch_name);
761                        }
762                        Ok::<(), anyhow::Error>(())
763                    }
764                })??;
765            }
766
767            Ok(())
768        }
769        .boxed()
770    }
771
772    fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>> {
773        let fs = self.fs.clone();
774        let executor = self.executor.clone();
775        let common_dir_path = self.common_dir_path.clone();
776        async move {
777            executor.simulate_random_delay().await;
778
779            if !force {
780                fs.with_git_state(&common_dir_path, false, |state| {
781                    if state.worktrees_requiring_force_delete.contains(&path) {
782                        bail!(
783                            "fatal: '{}' contains modified or untracked files, use --force to delete it",
784                            path.display()
785                        );
786                    }
787                    Ok::<(), anyhow::Error>(())
788                })??;
789            }
790
791            // Try to read the worktree's .git file to find its entry
792            // directory. If the working tree is already gone (e.g. the
793            // caller deleted it before asking git to clean up), fall back
794            // to scanning `.git/worktrees/*/gitdir` for a matching path,
795            // mirroring real git's behavior with `--force`.
796            let dot_git_file = path.join(".git");
797            let worktree_entry_dir = if let Ok(content) = fs.load(&dot_git_file).await {
798                let gitdir = content
799                    .strip_prefix("gitdir:")
800                    .context("invalid .git file in worktree")?
801                    .trim();
802                PathBuf::from(gitdir)
803            } else {
804                self.find_worktree_entry_dir_by_path(&path)
805                    .await
806                    .with_context(|| format!("no worktree found at path: {}", path.display()))?
807            };
808
809            // Remove the worktree checkout directory if it still exists.
810            fs.remove_dir(
811                &path,
812                RemoveOptions {
813                    recursive: true,
814                    ignore_if_not_exists: true,
815                },
816            )
817            .await?;
818
819            // Remove the .git/worktrees/<name>/ directory.
820            fs.remove_dir(
821                &worktree_entry_dir,
822                RemoveOptions {
823                    recursive: true,
824                    ignore_if_not_exists: false,
825                },
826            )
827            .await?;
828
829            // Emit a git event on the main .git directory so the scanner
830            // notices the change.
831            fs.with_git_state(&common_dir_path, true, |state| {
832                state.worktrees_requiring_force_delete.remove(&path);
833                Ok::<(), anyhow::Error>(())
834            })??;
835
836            Ok(())
837        }
838        .boxed()
839    }
840
841    fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
842        let fs = self.fs.clone();
843        let executor = self.executor.clone();
844        let common_dir_path = self.common_dir_path.clone();
845        async move {
846            executor.simulate_random_delay().await;
847
848            // Read the worktree's .git file to find its entry directory.
849            let dot_git_file = old_path.join(".git");
850            let content = fs
851                .load(&dot_git_file)
852                .await
853                .with_context(|| format!("no worktree found at path: {}", old_path.display()))?;
854            let gitdir = content
855                .strip_prefix("gitdir:")
856                .context("invalid .git file in worktree")?
857                .trim();
858            let worktree_entry_dir = PathBuf::from(gitdir);
859
860            // Move the worktree checkout directory.
861            fs.rename(
862                &old_path,
863                &new_path,
864                RenameOptions {
865                    overwrite: false,
866                    ignore_if_exists: false,
867                    create_parents: true,
868                },
869            )
870            .await?;
871
872            // Update the gitdir file in .git/worktrees/<name>/ to point to the
873            // new location.
874            let new_dot_git = new_path.join(".git");
875            fs.write_file_internal(
876                worktree_entry_dir.join("gitdir"),
877                new_dot_git.to_string_lossy().into_owned().into_bytes(),
878                false,
879            )?;
880
881            // Update the .git file in the moved worktree checkout.
882            fs.write_file_internal(
883                &new_dot_git,
884                format!("gitdir: {}", worktree_entry_dir.display()).into_bytes(),
885                false,
886            )?;
887
888            // Emit a git event on the main .git directory so the scanner
889            // notices the change.
890            fs.with_git_state(&common_dir_path, true, |_| {})?;
891
892            Ok(())
893        }
894        .boxed()
895    }
896
897    fn checkout_branch_in_worktree(
898        &self,
899        _branch_name: String,
900        _worktree_path: PathBuf,
901        _create: bool,
902    ) -> BoxFuture<'_, Result<()>> {
903        async { Ok(()) }.boxed()
904    }
905
906    fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
907        self.with_state_async(true, |state| {
908            state.current_branch_name = Some(name);
909            Ok(())
910        })
911    }
912
913    fn create_branch(
914        &self,
915        name: String,
916        _base_branch: Option<String>,
917    ) -> BoxFuture<'_, Result<()>> {
918        self.with_state_async(true, move |state| {
919            if let Some((remote, _)) = name.split_once('/')
920                && !state.remotes.contains_key(remote)
921            {
922                state.remotes.insert(remote.to_owned(), "".to_owned());
923            }
924            state.branches.insert(name);
925            Ok(())
926        })
927    }
928
929    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> {
930        self.with_state_async(true, move |state| {
931            if !state.branches.remove(&branch) {
932                bail!("no such branch: {branch}");
933            }
934            state.branches.insert(new_name.clone());
935            if state.current_branch_name == Some(branch) {
936                state.current_branch_name = Some(new_name);
937            }
938            Ok(())
939        })
940    }
941
942    fn delete_branch(
943        &self,
944        _is_remote: bool,
945        name: String,
946        force: bool,
947    ) -> BoxFuture<'_, Result<()>> {
948        self.with_state_async(true, move |state| {
949            if !force && state.branches_requiring_force_delete.contains(&name) {
950                bail!(
951                    "error: The branch '{name}' is not fully merged.\nIf you are sure you want to delete it, run 'git branch -D {name}'."
952                );
953            }
954            if !state.branches.remove(&name) {
955                bail!("no such branch: {name}");
956            }
957            state.branches_requiring_force_delete.remove(&name);
958            Ok(())
959        })
960    }
961
962    fn blame(
963        &self,
964        path: RepoPath,
965        _content: Rope,
966        _line_ending: LineEnding,
967    ) -> BoxFuture<'_, Result<git::blame::Blame>> {
968        self.with_state_async(false, move |state| {
969            state
970                .blames
971                .get(&path)
972                .with_context(|| format!("failed to get blame for {:?}", path))
973                .cloned()
974        })
975    }
976
977    fn stage_paths(
978        &self,
979        paths: Vec<RepoPath>,
980        _env: Arc<HashMap<String, String>>,
981    ) -> BoxFuture<'_, Result<()>> {
982        Box::pin(async move {
983            let contents = paths
984                .into_iter()
985                .map(|path| {
986                    let abs_path = self
987                        .dot_git_path
988                        .parent()
989                        .unwrap()
990                        .join(&path.as_std_path());
991                    Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
992                })
993                .collect::<Vec<_>>();
994            let contents = join_all(contents).await;
995            self.with_state_async(true, move |state| {
996                for (path, content) in contents {
997                    if let Some(content) = content {
998                        state.index_contents.insert(path, content);
999                    } else {
1000                        state.index_contents.remove(&path);
1001                    }
1002                }
1003                Ok(())
1004            })
1005            .await
1006        })
1007    }
1008
1009    fn unstage_paths(
1010        &self,
1011        paths: Vec<RepoPath>,
1012        _env: Arc<HashMap<String, String>>,
1013    ) -> BoxFuture<'_, Result<()>> {
1014        self.with_state_async(true, move |state| {
1015            for path in paths {
1016                match state.head_contents.get(&path) {
1017                    Some(content) => state.index_contents.insert(path, content.clone()),
1018                    None => state.index_contents.remove(&path),
1019                };
1020            }
1021            Ok(())
1022        })
1023    }
1024
1025    fn stash_paths(
1026        &self,
1027        _paths: Vec<RepoPath>,
1028        _env: Arc<HashMap<String, String>>,
1029    ) -> BoxFuture<'_, Result<()>> {
1030        unimplemented!()
1031    }
1032
1033    fn stash_pop(
1034        &self,
1035        _index: Option<usize>,
1036        _env: Arc<HashMap<String, String>>,
1037    ) -> BoxFuture<'_, Result<()>> {
1038        unimplemented!()
1039    }
1040
1041    fn stash_apply(
1042        &self,
1043        _index: Option<usize>,
1044        _env: Arc<HashMap<String, String>>,
1045    ) -> BoxFuture<'_, Result<()>> {
1046        unimplemented!()
1047    }
1048
1049    fn stash_drop(
1050        &self,
1051        _index: Option<usize>,
1052        _env: Arc<HashMap<String, String>>,
1053    ) -> BoxFuture<'_, Result<()>> {
1054        unimplemented!()
1055    }
1056
1057    fn commit(
1058        &self,
1059        _message: gpui::SharedString,
1060        _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
1061        options: CommitOptions,
1062        _askpass: AskPassDelegate,
1063        _env: Arc<HashMap<String, String>>,
1064    ) -> BoxFuture<'_, Result<()>> {
1065        self.with_state_async(true, move |state| {
1066            if !options.allow_empty && !options.amend && state.index_contents == state.head_contents
1067            {
1068                anyhow::bail!("nothing to commit (use allow_empty to create an empty commit)");
1069            }
1070
1071            let old_sha = state.refs.get("HEAD").cloned().unwrap_or_default();
1072            state.commit_history.push(FakeCommitSnapshot {
1073                head_contents: state.head_contents.clone(),
1074                index_contents: state.index_contents.clone(),
1075                sha: old_sha,
1076            });
1077
1078            state.head_contents = state.index_contents.clone();
1079
1080            let new_sha = format!("fake-commit-{}", state.commit_history.len());
1081            state.refs.insert("HEAD".into(), new_sha);
1082
1083            Ok(())
1084        })
1085    }
1086
1087    fn run_hook(
1088        &self,
1089        _hook: RunHook,
1090        _env: Arc<HashMap<String, String>>,
1091    ) -> BoxFuture<'_, Result<()>> {
1092        async { Ok(()) }.boxed()
1093    }
1094
1095    fn push(
1096        &self,
1097        _branch: String,
1098        _remote_branch: String,
1099        _remote: String,
1100        _options: Option<PushOptions>,
1101        _askpass: AskPassDelegate,
1102        _env: Arc<HashMap<String, String>>,
1103        _cx: AsyncApp,
1104    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
1105        unimplemented!()
1106    }
1107
1108    fn pull(
1109        &self,
1110        _branch: Option<String>,
1111        _remote: String,
1112        _rebase: bool,
1113        _askpass: AskPassDelegate,
1114        _env: Arc<HashMap<String, String>>,
1115        _cx: AsyncApp,
1116    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
1117        unimplemented!()
1118    }
1119
1120    fn fetch(
1121        &self,
1122        _fetch_options: FetchOptions,
1123        _askpass: AskPassDelegate,
1124        _env: Arc<HashMap<String, String>>,
1125        _cx: AsyncApp,
1126    ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
1127        unimplemented!()
1128    }
1129
1130    fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
1131        self.with_state_async(false, move |state| {
1132            let remotes = state
1133                .remotes
1134                .keys()
1135                .map(|r| Remote {
1136                    name: r.clone().into(),
1137                })
1138                .collect::<Vec<_>>();
1139            Ok(remotes)
1140        })
1141    }
1142
1143    fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
1144        unimplemented!()
1145    }
1146
1147    fn get_branch_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
1148        unimplemented!()
1149    }
1150
1151    fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
1152        future::ready(Ok(Vec::new())).boxed()
1153    }
1154
1155    fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
1156        future::ready(Ok(String::new())).boxed()
1157    }
1158
1159    fn diff_stat(
1160        &self,
1161        diff: git::repository::DiffStatType,
1162        path_prefixes: &[RepoPath],
1163    ) -> BoxFuture<'static, Result<git::status::GitDiffStat>> {
1164        fn count_lines(s: &str) -> u32 {
1165            if s.is_empty() {
1166                0
1167            } else {
1168                s.lines().count() as u32
1169            }
1170        }
1171
1172        fn matches_prefixes(path: &RepoPath, prefixes: &[RepoPath]) -> bool {
1173            if prefixes.is_empty() {
1174                return true;
1175            }
1176            prefixes.iter().any(|prefix| {
1177                let prefix_str = prefix.as_unix_str();
1178                if prefix_str == "." {
1179                    return true;
1180                }
1181                path == prefix || path.starts_with(&prefix)
1182            })
1183        }
1184
1185        let path_prefixes = path_prefixes.to_vec();
1186
1187        let workdir_path = self.dot_git_path.parent().unwrap().to_path_buf();
1188        let worktree_files: HashMap<RepoPath, String> = self
1189            .fs
1190            .files()
1191            .iter()
1192            .filter_map(|path| {
1193                let repo_path = path.strip_prefix(&workdir_path).ok()?;
1194                if repo_path.starts_with(".git") {
1195                    return None;
1196                }
1197                let content = self
1198                    .fs
1199                    .read_file_sync(path)
1200                    .ok()
1201                    .and_then(|bytes| String::from_utf8(bytes).ok())?;
1202                let repo_path = RelPath::new(repo_path, PathStyle::local()).ok()?;
1203                Some((RepoPath::from_rel_path(&repo_path), content))
1204            })
1205            .collect();
1206
1207        self.with_state_async(false, move |state| {
1208            let mut entries = Vec::new();
1209            let (old_files, new_files) = match diff {
1210                git::repository::DiffStatType::HeadToIndex => {
1211                    (&state.head_contents, &state.index_contents)
1212                }
1213                git::repository::DiffStatType::HeadToWorktree => {
1214                    (&state.head_contents, &worktree_files)
1215                }
1216                git::repository::DiffStatType::IndexToWorktree => {
1217                    (&state.index_contents, &worktree_files)
1218                }
1219            };
1220            let all_paths: HashSet<&RepoPath> = match diff {
1221                git::repository::DiffStatType::HeadToIndex => state
1222                    .head_contents
1223                    .keys()
1224                    .chain(state.index_contents.keys())
1225                    .collect(),
1226                git::repository::DiffStatType::HeadToWorktree => state
1227                    .head_contents
1228                    .keys()
1229                    .chain(
1230                        worktree_files
1231                            .keys()
1232                            .filter(|path| state.index_contents.contains_key(*path)),
1233                    )
1234                    .collect(),
1235                git::repository::DiffStatType::IndexToWorktree => {
1236                    state.index_contents.keys().collect()
1237                }
1238            };
1239            for path in all_paths {
1240                if !matches_prefixes(path, &path_prefixes) {
1241                    continue;
1242                }
1243                let old_file = old_files.get(path);
1244                let new_file = new_files.get(path);
1245                match (old_file, new_file) {
1246                    (Some(old), Some(new)) if old != new => {
1247                        entries.push((
1248                            path.clone(),
1249                            git::status::DiffStat {
1250                                added: count_lines(new),
1251                                deleted: count_lines(old),
1252                            },
1253                        ));
1254                    }
1255                    (Some(old), None) => {
1256                        entries.push((
1257                            path.clone(),
1258                            git::status::DiffStat {
1259                                added: 0,
1260                                deleted: count_lines(old),
1261                            },
1262                        ));
1263                    }
1264                    (None, Some(new)) => {
1265                        entries.push((
1266                            path.clone(),
1267                            git::status::DiffStat {
1268                                added: count_lines(new),
1269                                deleted: 0,
1270                            },
1271                        ));
1272                    }
1273                    _ => {}
1274                }
1275            }
1276            entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1277            Ok(git::status::GitDiffStat {
1278                entries: entries.into(),
1279            })
1280        })
1281        .boxed()
1282    }
1283
1284    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
1285        let executor = self.executor.clone();
1286        let fs = self.fs.clone();
1287        let checkpoints = self.checkpoints.clone();
1288        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
1289        async move {
1290            executor.simulate_random_delay().await;
1291            let oid = git::Oid::random(&mut *executor.rng().lock());
1292            let entry = fs.entry(&repository_dir_path)?;
1293            checkpoints.lock().insert(oid, entry);
1294            Ok(GitRepositoryCheckpoint { commit_sha: oid })
1295        }
1296        .boxed()
1297    }
1298
1299    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
1300        let executor = self.executor.clone();
1301        let fs = self.fs.clone();
1302        let checkpoints = self.checkpoints.clone();
1303        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
1304        async move {
1305            executor.simulate_random_delay().await;
1306            let checkpoints = checkpoints.lock();
1307            let entry = checkpoints
1308                .get(&checkpoint.commit_sha)
1309                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
1310            fs.insert_entry(&repository_dir_path, entry.clone())?;
1311            Ok(())
1312        }
1313        .boxed()
1314    }
1315
1316    fn create_archive_checkpoint(&self) -> BoxFuture<'_, Result<(String, String)>> {
1317        let executor = self.executor.clone();
1318        let fs = self.fs.clone();
1319        let checkpoints = self.checkpoints.clone();
1320        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
1321        async move {
1322            executor.simulate_random_delay().await;
1323            let staged_oid = git::Oid::random(&mut *executor.rng().lock());
1324            let unstaged_oid = git::Oid::random(&mut *executor.rng().lock());
1325            let entry = fs.entry(&repository_dir_path)?;
1326            checkpoints.lock().insert(staged_oid, entry.clone());
1327            checkpoints.lock().insert(unstaged_oid, entry);
1328            Ok((staged_oid.to_string(), unstaged_oid.to_string()))
1329        }
1330        .boxed()
1331    }
1332
1333    fn restore_archive_checkpoint(
1334        &self,
1335        // The fake filesystem doesn't model a separate index, so only the
1336        // unstaged (full working directory) snapshot is restored.
1337        _staged_sha: String,
1338        unstaged_sha: String,
1339    ) -> BoxFuture<'_, Result<()>> {
1340        match unstaged_sha.parse() {
1341            Ok(commit_sha) => self.restore_checkpoint(GitRepositoryCheckpoint { commit_sha }),
1342            Err(error) => async move {
1343                Err(anyhow::anyhow!(error).context("failed to parse unstaged SHA as Oid"))
1344            }
1345            .boxed(),
1346        }
1347    }
1348
1349    fn compare_checkpoints(
1350        &self,
1351        left: GitRepositoryCheckpoint,
1352        right: GitRepositoryCheckpoint,
1353    ) -> BoxFuture<'_, Result<bool>> {
1354        let executor = self.executor.clone();
1355        let checkpoints = self.checkpoints.clone();
1356        async move {
1357            executor.simulate_random_delay().await;
1358            let checkpoints = checkpoints.lock();
1359            let left = checkpoints
1360                .get(&left.commit_sha)
1361                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
1362            let right = checkpoints
1363                .get(&right.commit_sha)
1364                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
1365
1366            Ok(left == right)
1367        }
1368        .boxed()
1369    }
1370
1371    fn diff_checkpoints(
1372        &self,
1373        base_checkpoint: GitRepositoryCheckpoint,
1374        target_checkpoint: GitRepositoryCheckpoint,
1375    ) -> BoxFuture<'_, Result<String>> {
1376        let executor = self.executor.clone();
1377        let checkpoints = self.checkpoints.clone();
1378        async move {
1379            executor.simulate_random_delay().await;
1380            let checkpoints = checkpoints.lock();
1381            let base = checkpoints
1382                .get(&base_checkpoint.commit_sha)
1383                .context(format!(
1384                    "invalid base checkpoint: {}",
1385                    base_checkpoint.commit_sha
1386                ))?;
1387            let target = checkpoints
1388                .get(&target_checkpoint.commit_sha)
1389                .context(format!(
1390                    "invalid target checkpoint: {}",
1391                    target_checkpoint.commit_sha
1392                ))?;
1393
1394            fn collect_files(
1395                entry: &FakeFsEntry,
1396                prefix: String,
1397                out: &mut std::collections::BTreeMap<String, String>,
1398            ) {
1399                match entry {
1400                    FakeFsEntry::File { content, .. } => {
1401                        out.insert(prefix, String::from_utf8_lossy(content).into_owned());
1402                    }
1403                    FakeFsEntry::Dir { entries, .. } => {
1404                        for (name, child) in entries {
1405                            let path = if prefix.is_empty() {
1406                                name.clone()
1407                            } else {
1408                                format!("{prefix}/{name}")
1409                            };
1410                            collect_files(child, path, out);
1411                        }
1412                    }
1413                    FakeFsEntry::Symlink { .. } => {}
1414                }
1415            }
1416
1417            let mut base_files = std::collections::BTreeMap::new();
1418            let mut target_files = std::collections::BTreeMap::new();
1419            collect_files(base, String::new(), &mut base_files);
1420            collect_files(target, String::new(), &mut target_files);
1421
1422            let all_paths: std::collections::BTreeSet<&String> =
1423                base_files.keys().chain(target_files.keys()).collect();
1424
1425            let mut diff = String::new();
1426            for path in all_paths {
1427                match (base_files.get(path), target_files.get(path)) {
1428                    (Some(base_content), Some(target_content))
1429                        if base_content != target_content =>
1430                    {
1431                        diff.push_str(&format!("diff --git a/{path} b/{path}\n"));
1432                        diff.push_str(&format!("--- a/{path}\n"));
1433                        diff.push_str(&format!("+++ b/{path}\n"));
1434                        for line in base_content.lines() {
1435                            diff.push_str(&format!("-{line}\n"));
1436                        }
1437                        for line in target_content.lines() {
1438                            diff.push_str(&format!("+{line}\n"));
1439                        }
1440                    }
1441                    (Some(_), None) => {
1442                        diff.push_str(&format!("diff --git a/{path} /dev/null\n"));
1443                        diff.push_str("deleted file\n");
1444                    }
1445                    (None, Some(_)) => {
1446                        diff.push_str(&format!("diff --git /dev/null b/{path}\n"));
1447                        diff.push_str("new file\n");
1448                    }
1449                    _ => {}
1450                }
1451            }
1452            Ok(diff)
1453        }
1454        .boxed()
1455    }
1456
1457    fn default_branch(
1458        &self,
1459        include_remote_name: bool,
1460    ) -> BoxFuture<'_, Result<Option<SharedString>>> {
1461        async move {
1462            Ok(Some(if include_remote_name {
1463                "origin/main".into()
1464            } else {
1465                "main".into()
1466            }))
1467        }
1468        .boxed()
1469    }
1470
1471    fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
1472        self.with_state_async(true, move |state| {
1473            state.remotes.insert(name, url);
1474            Ok(())
1475        })
1476    }
1477
1478    fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
1479        self.with_state_async(true, move |state| {
1480            state.branches.retain(|branch| {
1481                branch
1482                    .split_once('/')
1483                    .is_none_or(|(remote, _)| remote != name)
1484            });
1485            state.remotes.remove(&name);
1486            Ok(())
1487        })
1488    }
1489
1490    fn initial_graph_data(
1491        &self,
1492        _log_source: LogSource,
1493        _log_order: LogOrder,
1494        request_tx: Sender<Vec<Arc<InitialGraphCommitData>>>,
1495    ) -> BoxFuture<'_, Result<()>> {
1496        let fs = self.fs.clone();
1497        let dot_git_path = self.dot_git_path.clone();
1498        async move {
1499            let (graph_commits, simulated_error) =
1500                fs.with_git_state(&dot_git_path, false, |state| {
1501                    (
1502                        state.graph_commits.clone(),
1503                        state.simulated_graph_error.clone(),
1504                    )
1505                })?;
1506
1507            if let Some(error) = simulated_error {
1508                anyhow::bail!("{}", error);
1509            }
1510
1511            for chunk in graph_commits.chunks(GRAPH_CHUNK_SIZE) {
1512                request_tx.send(chunk.to_vec()).await.ok();
1513            }
1514            Ok(())
1515        }
1516        .boxed()
1517    }
1518
1519    fn search_commits(
1520        &self,
1521        _log_source: LogSource,
1522        search_args: SearchCommitArgs,
1523        request_tx: Sender<Oid>,
1524    ) -> BoxFuture<'_, Result<()>> {
1525        async move {
1526            let hash_query = commit_hash_search_query(search_args.query.as_str())
1527                .map(|query| query.to_ascii_lowercase());
1528            let message_query = if search_args.case_sensitive {
1529                search_args.query.to_string()
1530            } else {
1531                search_args.query.to_lowercase()
1532            };
1533
1534            let matching_shas = self.fs.with_git_state(&self.dot_git_path, false, |state| {
1535                state
1536                    .commit_data
1537                    .iter()
1538                    .filter_map(|(sha, entry)| {
1539                        let FakeCommitDataEntry::Success(commit_data) = entry else {
1540                            return None;
1541                        };
1542                        if let Some(hash_query) = hash_query.as_ref() {
1543                            return sha
1544                                .to_string()
1545                                .to_ascii_lowercase()
1546                                .starts_with(hash_query)
1547                                .then_some(*sha);
1548                        }
1549                        let message = if search_args.case_sensitive {
1550                            commit_data.message.to_string()
1551                        } else {
1552                            commit_data.message.to_lowercase()
1553                        };
1554                        message.contains(&message_query).then_some(*sha)
1555                    })
1556                    .collect::<Vec<_>>()
1557            })?;
1558
1559            for sha in matching_shas {
1560                if request_tx.send(sha).await.is_err() {
1561                    break;
1562                }
1563            }
1564
1565            Ok(())
1566        }
1567        .boxed()
1568    }
1569
1570    fn file_history_changed_files(
1571        &self,
1572        paths: Vec<RepoPath>,
1573        _commit_limit: usize,
1574    ) -> BoxFuture<'_, Result<Vec<FileHistoryChangedFileSets>>> {
1575        async move { Ok(vec![FileHistoryChangedFileSets::default(); paths.len()]) }.boxed()
1576    }
1577
1578    fn commit_data_reader(&self) -> Result<CommitDataReader> {
1579        let fs = self.fs.clone();
1580        let dot_git_path = self.dot_git_path.clone();
1581        let executor = self.executor.clone();
1582        Ok(CommitDataReader::for_test(executor, move |sha| {
1583            fs.with_git_state(&dot_git_path, false, |state| {
1584                let commit = state
1585                    .commit_data
1586                    .get(&sha)
1587                    .context(format!("graph commit data not found for {sha}"))?;
1588
1589                match commit {
1590                    FakeCommitDataEntry::Success(data) => Ok(data.clone()),
1591                    FakeCommitDataEntry::Fail(_) => {
1592                        bail!("simulated commit data read failure for {sha}")
1593                    }
1594                }
1595            })?
1596        }))
1597    }
1598
1599    fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> {
1600        self.edit_ref(RefEdit::Update { ref_name, commit })
1601    }
1602
1603    fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> {
1604        self.edit_ref(RefEdit::Delete { ref_name })
1605    }
1606
1607    fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {
1608        async { Ok(()) }.boxed()
1609    }
1610
1611    fn set_trusted(&self, trusted: bool) {
1612        self.is_trusted
1613            .store(trusted, std::sync::atomic::Ordering::Release);
1614    }
1615
1616    fn is_trusted(&self) -> bool {
1617        self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
1618    }
1619}
1620
Served at tenant.openagents/omega Member data and write actions are omitted.