Skip to repository content1620 lines · 60.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:05:13.704Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
fake_git_repo.rs
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