Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:01:57.707Z 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

fs.rs

3454 lines · 118.2 KB · rust
1pub mod fs_watcher;
2
3pub use fs_watcher::requires_poll_watcher;
4
5use parking_lot::Mutex;
6use slotmap::{KeyData, SlotMap};
7use std::ffi::OsString;
8use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
9use std::time::Instant;
10use util::maybe;
11
12use anyhow::{Context as _, Result, anyhow};
13use futures::stream::iter;
14use gpui::App;
15use gpui::BackgroundExecutor;
16use gpui::Global;
17use gpui::ReadGlobal as _;
18use gpui::SharedString;
19#[cfg(unix)]
20use std::ffi::CString;
21use util::command::new_command;
22
23#[cfg(unix)]
24use std::os::fd::{AsFd, AsRawFd};
25#[cfg(unix)]
26use std::os::unix::ffi::OsStrExt;
27
28#[cfg(unix)]
29use std::os::unix::fs::{FileTypeExt, MetadataExt};
30
31#[cfg(any(target_os = "macos", target_os = "freebsd"))]
32use std::mem::MaybeUninit;
33
34use async_tar::Archive;
35use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};
36use git::repository::{GitRepository, RealGitRepository};
37use is_executable::IsExecutable;
38use rope::Rope;
39use serde::{Deserialize, Serialize};
40use smol::io::AsyncWriteExt;
41#[cfg(feature = "test-support")]
42use std::path::Component;
43use std::{
44    io::{self, Write},
45    path::{Path, PathBuf},
46    pin::Pin,
47    sync::Arc,
48    time::{Duration, SystemTime, UNIX_EPOCH},
49};
50use tempfile::TempDir;
51use text::LineEnding;
52
53#[cfg(feature = "test-support")]
54mod fake_git_repo;
55#[cfg(feature = "test-support")]
56use collections::{BTreeMap, btree_map};
57#[cfg(feature = "test-support")]
58use fake_git_repo::{FakeCommitDataEntry, FakeGitRepositoryState};
59#[cfg(feature = "test-support")]
60use git::{
61    repository::{CommitData, InitialGraphCommitData, RepoPath, Worktree, repo_path},
62    status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
63};
64#[cfg(feature = "test-support")]
65use path::normalize_path;
66
67#[cfg(feature = "test-support")]
68use smol::io::AsyncReadExt;
69#[cfg(feature = "test-support")]
70use std::ffi::OsStr;
71
72pub trait Watcher: Send + Sync {
73    fn add(&self, path: &Path) -> Result<()>;
74    fn remove(&self, path: &Path) -> Result<()>;
75}
76
77#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
78pub enum PathEventKind {
79    Removed,
80    Created,
81    Changed,
82    Rescan,
83}
84
85#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
86pub struct PathEvent {
87    pub path: PathBuf,
88    pub kind: Option<PathEventKind>,
89}
90
91impl From<PathEvent> for PathBuf {
92    fn from(event: PathEvent) -> Self {
93        event.path
94    }
95}
96
97#[async_trait::async_trait]
98pub trait Fs: Send + Sync {
99    async fn create_dir(&self, path: &Path) -> Result<()>;
100    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>;
101    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
102    async fn create_file_with(
103        &self,
104        path: &Path,
105        content: Pin<&mut (dyn AsyncRead + Send)>,
106    ) -> Result<()>;
107    async fn extract_tar_file(
108        &self,
109        path: &Path,
110        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
111    ) -> Result<()>;
112    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
113    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
114
115    /// Removes a directory from the filesystem.
116    /// There is no expectation that the directory will be preserved in the
117    /// system trash.
118    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
119
120    /// Moves a file or directory to the system trash.
121    /// Returns a [`TrashedEntry`] that can be used to keep track of the
122    /// location of the trashed item in the system's trash.
123    async fn trash(&self, path: &Path, options: RemoveOptions) -> Result<TrashId>;
124
125    /// Removes a file from the filesystem.
126    /// There is no expectation that the file will be preserved in the system
127    /// trash.
128    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
129
130    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>;
131    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>>;
132    async fn load(&self, path: &Path) -> Result<String> {
133        Ok(String::from_utf8(self.load_bytes(path).await?)?)
134    }
135    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
136    async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
137    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
138    async fn write(&self, path: &Path, content: &[u8]) -> Result<()>;
139    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
140    async fn is_file(&self, path: &Path) -> bool;
141    async fn is_dir(&self, path: &Path) -> bool;
142    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
143    async fn read_link(&self, path: &Path) -> Result<PathBuf>;
144    async fn read_dir(
145        &self,
146        path: &Path,
147    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
148
149    async fn watch(
150        &self,
151        path: &Path,
152        latency: Duration,
153    ) -> (
154        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
155        Arc<dyn Watcher>,
156    );
157
158    fn open_repo(
159        &self,
160        abs_dot_git: &Path,
161        system_git_binary_path: Option<&Path>,
162    ) -> Result<Arc<dyn GitRepository>>;
163    async fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String)
164    -> Result<()>;
165    async fn git_clone(&self, abs_work_directory: &Path, repo_url: &str) -> Result<()>;
166    async fn git_config(&self, abs_work_directory: &Path, args: Vec<String>) -> Result<String>;
167    fn is_fake(&self) -> bool;
168    async fn is_case_sensitive(&self) -> bool;
169    fn subscribe_to_jobs(&self) -> JobEventReceiver;
170
171    /// Restores a given `TrashedEntry`, moving it from the system's trash back
172    /// to the original path.
173    async fn restore(&self, item: TrashId) -> std::result::Result<PathBuf, TrashRestoreError>;
174
175    #[cfg(feature = "test-support")]
176    fn as_fake(&self) -> Arc<FakeFs> {
177        panic!("called as_fake on a real fs");
178    }
179}
180
181// We use our own type rather than `trash::TrashItem` directly to avoid carrying
182// over fields we don't need (e.g. `time_deleted`) and to insulate callers and
183// tests from changes to that crate's API surface.
184/// Represents a file or directory that has been moved to the system trash,
185/// retaining enough information to restore it to its original location.
186#[derive(Clone, PartialEq, Debug)]
187struct TrashedEntry {
188    /// Platform-specific identifier for the file/directory in the trash.
189    ///
190    /// * Freedesktop – Path to the `.trashinfo` file.
191    /// * macOS & Windows – Full path to the file/directory in the system's
192    /// trash.
193    pub id: OsString,
194    /// Name of the file/directory at the time of trashing, including extension.
195    pub name: OsString,
196    /// Absolute path to the parent directory at the time of trashing.
197    pub original_parent: PathBuf,
198}
199
200impl From<trash::TrashItem> for TrashedEntry {
201    fn from(item: trash::TrashItem) -> Self {
202        Self {
203            id: item.id,
204            name: item.name,
205            original_parent: item.original_parent,
206        }
207    }
208}
209
210impl TrashedEntry {
211    fn into_trash_item(self) -> trash::TrashItem {
212        trash::TrashItem {
213            id: self.id,
214            name: self.name,
215            original_parent: self.original_parent,
216            // `TrashedEntry` doesn't preserve `time_deleted` as we don't
217            // currently need it for restore, so we default it to 0 here.
218            time_deleted: 0,
219        }
220    }
221}
222
223#[derive(Debug, thiserror::Error)]
224pub enum TrashRestoreError {
225    #[error("The specified `path` ({}) was not found in the system's trash.", path.display())]
226    NotFound { path: PathBuf },
227    #[error("File or directory ({}) already exists at the restore destination.", path.display())]
228    Collision { path: PathBuf },
229    // This should never occur, the only way to get a TrashId is to undo
230    // consumes the Change::Trashed. We worry about remoting duplicate messages
231    // we do not want to crash the app then which is why this error is there.
232    #[error("The item was already restored")]
233    AlreadyRestored,
234    #[error("Unknown error ({description})")]
235    Unknown { description: String },
236}
237
238impl From<trash::Error> for TrashRestoreError {
239    fn from(err: trash::Error) -> Self {
240        match err {
241            trash::Error::RestoreCollision { path, .. } => Self::Collision { path },
242            trash::Error::Unknown { description } => Self::Unknown { description },
243            other => Self::Unknown {
244                description: other.to_string(),
245            },
246        }
247    }
248}
249
250struct GlobalFs(Arc<dyn Fs>);
251
252impl Global for GlobalFs {}
253
254impl dyn Fs {
255    /// Returns the global [`Fs`].
256    pub fn global(cx: &App) -> Arc<Self> {
257        GlobalFs::global(cx).0.clone()
258    }
259
260    /// Sets the global [`Fs`].
261    pub fn set_global(fs: Arc<Self>, cx: &mut App) {
262        cx.set_global(GlobalFs(fs));
263    }
264}
265
266#[derive(Copy, Clone, Default)]
267pub struct CreateOptions {
268    pub overwrite: bool,
269    pub ignore_if_exists: bool,
270}
271
272#[derive(Copy, Clone, Default)]
273pub struct CopyOptions {
274    pub overwrite: bool,
275    pub ignore_if_exists: bool,
276}
277
278#[derive(Copy, Clone, Default)]
279pub struct RenameOptions {
280    pub overwrite: bool,
281    pub ignore_if_exists: bool,
282    /// Whether to create parent directories if they do not exist.
283    pub create_parents: bool,
284}
285
286#[derive(Copy, Clone, Default)]
287pub struct RemoveOptions {
288    pub recursive: bool,
289    pub ignore_if_not_exists: bool,
290}
291
292#[derive(Copy, Clone, Debug)]
293pub struct Metadata {
294    pub inode: u64,
295    pub mtime: MTime,
296    pub is_symlink: bool,
297    pub is_dir: bool,
298    pub len: u64,
299    pub is_fifo: bool,
300    pub is_executable: bool,
301    pub is_writable: bool,
302}
303
304/// Filesystem modification time. The purpose of this newtype is to discourage use of operations
305/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using
306/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it
307/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).
308///
309/// Do not derive Ord, PartialOrd, or arithmetic operation traits.
310#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
311#[serde(transparent)]
312pub struct MTime(SystemTime);
313
314pub type JobId = usize;
315
316#[derive(Clone, Debug)]
317pub struct JobInfo {
318    pub start: Instant,
319    pub message: SharedString,
320    pub id: JobId,
321}
322
323#[derive(Debug, Clone)]
324pub enum JobEvent {
325    Started { info: JobInfo },
326    Completed { id: JobId },
327}
328
329pub type JobEventSender = futures::channel::mpsc::UnboundedSender<JobEvent>;
330pub type JobEventReceiver = futures::channel::mpsc::UnboundedReceiver<JobEvent>;
331
332struct JobTracker {
333    id: JobId,
334    subscribers: Arc<Mutex<Vec<JobEventSender>>>,
335}
336
337impl JobTracker {
338    fn new(info: JobInfo, subscribers: Arc<Mutex<Vec<JobEventSender>>>) -> Self {
339        let id = info.id;
340        {
341            let mut subs = subscribers.lock();
342            subs.retain(|sender| {
343                sender
344                    .unbounded_send(JobEvent::Started { info: info.clone() })
345                    .is_ok()
346            });
347        }
348        Self { id, subscribers }
349    }
350}
351
352impl Drop for JobTracker {
353    fn drop(&mut self) {
354        let mut subs = self.subscribers.lock();
355        subs.retain(|sender| {
356            sender
357                .unbounded_send(JobEvent::Completed { id: self.id })
358                .is_ok()
359        });
360    }
361}
362
363impl MTime {
364    /// Conversion intended for persistence and testing.
365    pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self {
366        MTime(UNIX_EPOCH + Duration::new(secs, nanos))
367    }
368
369    /// Conversion intended for persistence.
370    pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> {
371        self.0
372            .duration_since(UNIX_EPOCH)
373            .ok()
374            .map(|duration| (duration.as_secs(), duration.subsec_nanos()))
375    }
376
377    /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including
378    /// "_for_user" is to discourage misuse - this method should not be used when making decisions
379    /// about file dirtiness.
380    pub fn timestamp_for_user(self) -> SystemTime {
381        self.0
382    }
383
384    /// Temporary method to split out the behavior changes from introduction of this newtype.
385    pub fn bad_is_greater_than(self, other: MTime) -> bool {
386        self.0 > other.0
387    }
388}
389
390impl From<proto::Timestamp> for MTime {
391    fn from(timestamp: proto::Timestamp) -> Self {
392        MTime(timestamp.into())
393    }
394}
395
396impl From<MTime> for proto::Timestamp {
397    fn from(mtime: MTime) -> Self {
398        mtime.0.into()
399    }
400}
401
402slotmap::new_key_type! { pub struct TrashId; }
403
404impl TrashId {
405    pub fn from_proto(value: u64) -> Self {
406        KeyData::from_ffi(value).into()
407    }
408
409    pub fn to_proto(self) -> u64 {
410        self.0.as_ffi()
411    }
412}
413
414pub struct RealFs {
415    bundled_git_binary_path: Option<PathBuf>,
416    executor: BackgroundExecutor,
417    next_job_id: Arc<AtomicUsize>,
418    job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>,
419    trash: Arc<Mutex<SlotMap<TrashId, TrashedEntry>>>,
420    is_case_sensitive: AtomicU8,
421}
422
423pub trait FileHandle: Send + Sync + std::fmt::Debug {
424    fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>;
425}
426
427impl FileHandle for std::fs::File {
428    #[cfg(target_os = "macos")]
429    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
430        use std::{
431            ffi::{CStr, OsStr},
432            os::unix::ffi::OsStrExt,
433        };
434
435        let fd = self.as_fd();
436        let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit();
437
438        let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
439        anyhow::ensure!(result != -1, "fcntl returned -1");
440
441        // SAFETY: `fcntl` will initialize the path buffer.
442        let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) };
443        anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle");
444        let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
445        Ok(path)
446    }
447
448    #[cfg(target_os = "linux")]
449    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
450        let fd = self.as_fd();
451        let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd());
452        let new_path = std::fs::read_link(fd_path)?;
453        if new_path
454            .file_name()
455            .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)"))
456        {
457            anyhow::bail!("file was deleted")
458        };
459
460        Ok(new_path)
461    }
462
463    #[cfg(target_os = "freebsd")]
464    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
465        use std::{
466            ffi::{CStr, OsStr},
467            os::unix::ffi::OsStrExt,
468        };
469
470        let fd = self.as_fd();
471        let mut kif = MaybeUninit::<libc::kinfo_file>::uninit();
472        kif.kf_structsize = libc::KINFO_FILE_SIZE;
473
474        let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) };
475        anyhow::ensure!(result != -1, "fcntl returned -1");
476
477        // SAFETY: `fcntl` will initialize the kif.
478        let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) };
479        anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle");
480        let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
481        Ok(path)
482    }
483
484    #[cfg(target_os = "windows")]
485    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
486        use std::ffi::OsString;
487        use std::os::windows::ffi::OsStringExt;
488        use std::os::windows::io::AsRawHandle;
489
490        use windows::Win32::Foundation::HANDLE;
491        use windows::Win32::Storage::FileSystem::{
492            FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW,
493        };
494
495        let handle = HANDLE(self.as_raw_handle() as _);
496
497        // Query required buffer size (in wide chars)
498        let required_len =
499            unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) };
500        anyhow::ensure!(
501            required_len != 0,
502            "GetFinalPathNameByHandleW returned 0 length"
503        );
504
505        // Allocate buffer and retrieve the path
506        let mut buf: Vec<u16> = vec![0u16; required_len as usize + 1];
507        let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) };
508        anyhow::ensure!(
509            written != 0,
510            "GetFinalPathNameByHandleW failed to write path"
511        );
512
513        let os_str: OsString = OsString::from_wide(&buf[..written as usize]);
514        anyhow::ensure!(!os_str.is_empty(), "Could find a path for the file handle");
515        Ok(PathBuf::from(os_str))
516    }
517}
518
519pub struct RealWatcher {}
520
521impl RealFs {
522    pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
523        Self {
524            bundled_git_binary_path: git_binary_path,
525            executor,
526            next_job_id: Arc::new(AtomicUsize::new(0)),
527            job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
528            trash: Arc::new(Mutex::new(SlotMap::with_key())),
529            is_case_sensitive: Default::default(),
530        }
531    }
532
533    #[cfg(target_os = "windows")]
534    fn canonicalize(path: &Path) -> Result<PathBuf> {
535        use std::ffi::OsString;
536        use std::os::windows::ffi::OsStringExt;
537        use windows::Win32::Storage::FileSystem::GetVolumePathNameW;
538        use windows::core::HSTRING;
539
540        // std::fs::canonicalize resolves mapped network paths to UNC paths, which can
541        // confuse some software. To mitigate this, we canonicalize the input, then rebase
542        // the result onto the input's original volume root if both paths are on the same
543        // volume. This keeps the same drive letter or mount point the caller used.
544
545        let abs_path = if path.is_relative() {
546            std::env::current_dir()?.join(path)
547        } else {
548            path.to_path_buf()
549        };
550
551        let path_hstring = HSTRING::from(abs_path.as_os_str());
552        let mut vol_buf = vec![0u16; abs_path.as_os_str().len() + 2];
553        unsafe { GetVolumePathNameW(&path_hstring, &mut vol_buf)? };
554        let volume_root = {
555            let len = vol_buf
556                .iter()
557                .position(|&c| c == 0)
558                .unwrap_or(vol_buf.len());
559            PathBuf::from(OsString::from_wide(&vol_buf[..len]))
560        };
561
562        let resolved_path = dunce::canonicalize(&abs_path)?;
563        let resolved_root = dunce::canonicalize(&volume_root)?;
564
565        if let Ok(relative) = resolved_path.strip_prefix(&resolved_root) {
566            let mut result = volume_root;
567            result.push(relative);
568            Ok(result)
569        } else {
570            Ok(resolved_path)
571        }
572    }
573}
574
575#[cfg(any(target_os = "macos", target_os = "linux"))]
576fn rename_without_replace(source: &Path, target: &Path) -> io::Result<()> {
577    let source = path_to_c_string(source)?;
578    let target = path_to_c_string(target)?;
579
580    #[cfg(target_os = "macos")]
581    let result = unsafe { libc::renamex_np(source.as_ptr(), target.as_ptr(), libc::RENAME_EXCL) };
582
583    #[cfg(target_os = "linux")]
584    let result = unsafe {
585        libc::syscall(
586            libc::SYS_renameat2,
587            libc::AT_FDCWD,
588            source.as_ptr(),
589            libc::AT_FDCWD,
590            target.as_ptr(),
591            libc::RENAME_NOREPLACE,
592        )
593    };
594
595    if result == 0 {
596        Ok(())
597    } else {
598        Err(io::Error::last_os_error())
599    }
600}
601
602#[cfg(target_os = "windows")]
603fn rename_without_replace(source: &Path, target: &Path) -> io::Result<()> {
604    use std::os::windows::ffi::OsStrExt;
605
606    use windows::Win32::Storage::FileSystem::{MOVE_FILE_FLAGS, MoveFileExW};
607    use windows::core::PCWSTR;
608
609    let source: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
610    let target: Vec<u16> = target.as_os_str().encode_wide().chain(Some(0)).collect();
611
612    unsafe {
613        MoveFileExW(
614            PCWSTR(source.as_ptr()),
615            PCWSTR(target.as_ptr()),
616            MOVE_FILE_FLAGS::default(),
617        )
618    }
619    .map_err(|_| io::Error::last_os_error())
620}
621
622#[cfg(any(target_os = "macos", target_os = "linux"))]
623fn path_to_c_string(path: &Path) -> io::Result<CString> {
624    CString::new(path.as_os_str().as_bytes()).map_err(|_| {
625        io::Error::new(
626            io::ErrorKind::InvalidInput,
627            format!("path contains interior NUL: {}", path.display()),
628        )
629    })
630}
631
632#[async_trait::async_trait]
633impl Fs for RealFs {
634    async fn create_dir(&self, path: &Path) -> Result<()> {
635        Ok(smol::fs::create_dir_all(path).await?)
636    }
637
638    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
639        #[cfg(unix)]
640        smol::fs::unix::symlink(target, path).await?;
641
642        #[cfg(windows)]
643        if smol::fs::metadata(&target).await?.is_dir() {
644            let status = new_command("cmd")
645                .args(["/C", "mklink", "/J"])
646                .args([path, target.as_path()])
647                .status()
648                .await?;
649
650            if !status.success() {
651                return Err(anyhow::anyhow!(
652                    "Failed to create junction from {:?} to {:?}",
653                    path,
654                    target
655                ));
656            }
657        } else {
658            smol::fs::windows::symlink_file(target, path).await?
659        }
660
661        Ok(())
662    }
663
664    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
665        let mut open_options = smol::fs::OpenOptions::new();
666        open_options.write(true).create(true);
667        if options.overwrite {
668            open_options.truncate(true);
669        } else if !options.ignore_if_exists {
670            open_options.create_new(true);
671        }
672        open_options
673            .open(path)
674            .await
675            .with_context(|| format!("Failed to create file at {:?}", path))?;
676        Ok(())
677    }
678
679    async fn create_file_with(
680        &self,
681        path: &Path,
682        content: Pin<&mut (dyn AsyncRead + Send)>,
683    ) -> Result<()> {
684        let mut file = smol::fs::File::create(&path)
685            .await
686            .with_context(|| format!("Failed to create file at {:?}", path))?;
687        futures::io::copy(content, &mut file).await?;
688        Ok(())
689    }
690
691    async fn extract_tar_file(
692        &self,
693        path: &Path,
694        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
695    ) -> Result<()> {
696        content.unpack(path).await?;
697        Ok(())
698    }
699
700    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
701        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
702            if options.ignore_if_exists {
703                return Ok(());
704            } else {
705                anyhow::bail!("{target:?} already exists");
706            }
707        }
708
709        smol::fs::copy(source, target).await?;
710        Ok(())
711    }
712
713    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
714        if options.create_parents {
715            if let Some(parent) = target.parent() {
716                self.create_dir(parent).await?;
717            }
718        }
719
720        if options.overwrite {
721            smol::fs::rename(source, target).await?;
722            return Ok(());
723        }
724
725        let use_metadata_fallback = {
726            #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
727            {
728                let source = source.to_path_buf();
729                let target = target.to_path_buf();
730                match self
731                    .executor
732                    .spawn(async move { rename_without_replace(&source, &target) })
733                    .await
734                {
735                    Ok(()) => return Ok(()),
736                    Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
737                        if options.ignore_if_exists {
738                            return Ok(());
739                        }
740                        return Err(error.into());
741                    }
742                    Err(error)
743                        if error.raw_os_error().is_some_and(|code| {
744                            code == libc::ENOSYS
745                                || code == libc::ENOTSUP
746                                || code == libc::EOPNOTSUPP
747                                || code == libc::EINVAL
748                        }) =>
749                    {
750                        // For case when filesystem or kernel does not support atomic no-overwrite rename.
751                        // EINVAL is returned by FUSE-based filesystems (e.g. NTFS via ntfs-3g)
752                        // that don't support RENAME_NOREPLACE.
753                        true
754                    }
755                    Err(error) => return Err(error.into()),
756                }
757            }
758
759            #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
760            {
761                // For platforms which do not have an atomic no-overwrite rename yet.
762                true
763            }
764        };
765
766        if use_metadata_fallback && smol::fs::metadata(target).await.is_ok() {
767            if options.ignore_if_exists {
768                return Ok(());
769            } else {
770                anyhow::bail!("{target:?} already exists");
771            }
772        }
773
774        smol::fs::rename(source, target).await?;
775        Ok(())
776    }
777
778    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
779        let result = if options.recursive {
780            smol::fs::remove_dir_all(path).await
781        } else {
782            smol::fs::remove_dir(path).await
783        };
784        match result {
785            Ok(()) => Ok(()),
786            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
787                Ok(())
788            }
789            Err(err) => Err(err)?,
790        }
791    }
792
793    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
794        #[cfg(windows)]
795        if let Ok(Some(metadata)) = self.metadata(path).await
796            && metadata.is_symlink
797            && metadata.is_dir
798        {
799            self.remove_dir(
800                path,
801                RemoveOptions {
802                    recursive: false,
803                    ignore_if_not_exists: true,
804                },
805            )
806            .await?;
807            return Ok(());
808        }
809
810        match smol::fs::remove_file(path).await {
811            Ok(()) => Ok(()),
812            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
813                Ok(())
814            }
815            Err(err) => Err(err)?,
816        }
817    }
818
819    async fn trash(&self, path: &Path, _options: RemoveOptions) -> Result<TrashId> {
820        // We must make the path absolute or trash will make a weird abomination
821        // of the zed working directory (not usually the worktree) and whatever
822        // the path variable holds.
823        // We deliberately use `std::path::absolute` instead of `canonicalize`
824        // to avoid resolving symlinks. Otherwise trashing a symlink would trash
825        // its target and leave the link behind.
826        let path = std::path::absolute(path).context("Could not make the path absolute")?;
827
828        let entry = smol::unblock(move || trash::delete_with_info(path))
829            .await
830            .context("Could not trash file or dir")?
831            .into();
832
833        Ok(self.trash.lock().insert(entry))
834    }
835
836    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>> {
837        Ok(Box::new(std::fs::File::open(path)?))
838    }
839
840    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
841        let mut options = std::fs::OpenOptions::new();
842        options.read(true);
843        #[cfg(windows)]
844        {
845            use std::os::windows::fs::OpenOptionsExt;
846            options.custom_flags(windows::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS.0);
847        }
848        Ok(Arc::new(options.open(path)?))
849    }
850
851    async fn load(&self, path: &Path) -> Result<String> {
852        let path = path.to_path_buf();
853        self.executor
854            .spawn(async move {
855                std::fs::read_to_string(&path)
856                    .with_context(|| format!("Failed to read file {}", path.display()))
857            })
858            .await
859    }
860
861    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
862        let path = path.to_path_buf();
863        let bytes = self
864            .executor
865            .spawn(async move { std::fs::read(path) })
866            .await?;
867        Ok(bytes)
868    }
869
870    #[cfg(not(target_os = "windows"))]
871    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
872        smol::unblock(move || {
873            // Use the directory of the destination as temp dir to avoid
874            // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
875            // See https://github.com/zed-industries/zed/pull/8437 for more details.
876            let mut tmp_file =
877                tempfile::NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?;
878            tmp_file.write_all(data.as_bytes())?;
879            tmp_file.persist(path)?;
880            anyhow::Ok(())
881        })
882        .await?;
883
884        Ok(())
885    }
886
887    #[cfg(target_os = "windows")]
888    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
889        smol::unblock(move || {
890            // If temp dir is set to a different drive than the destination,
891            // we receive error:
892            //
893            // failed to persist temporary file:
894            // The system cannot move the file to a different disk drive. (os error 17)
895            //
896            // This is because `ReplaceFileW` does not support cross volume moves.
897            // See the remark section: "The backup file, replaced file, and replacement file must all reside on the same volume."
898            // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew#remarks
899            //
900            // So we use the directory of the destination as a temp dir to avoid it.
901            // https://github.com/zed-industries/zed/issues/16571
902            let temp_dir = TempDir::new_in(path.parent().unwrap_or(paths::temp_dir()))?;
903            let temp_file = {
904                let temp_file_path = temp_dir.path().join("temp_file");
905                let mut file = std::fs::File::create_new(&temp_file_path)?;
906                file.write_all(data.as_bytes())?;
907                temp_file_path
908            };
909            atomic_replace(path.as_path(), temp_file.as_path())?;
910            anyhow::Ok(())
911        })
912        .await?;
913        Ok(())
914    }
915
916    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
917        let buffer_size = text.summary().len.min(10 * 1024);
918        if let Some(path) = path.parent() {
919            self.create_dir(path)
920                .await
921                .with_context(|| format!("Failed to create directory at {:?}", path))?;
922        }
923        let file = smol::fs::File::create(path)
924            .await
925            .with_context(|| format!("Failed to create file at {:?}", path))?;
926        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
927        for chunk in text::chunks_with_line_ending(text, line_ending) {
928            writer.write_all(chunk.as_bytes()).await?;
929        }
930        writer.flush().await?;
931        Ok(())
932    }
933
934    async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
935        if let Some(path) = path.parent() {
936            self.create_dir(path)
937                .await
938                .with_context(|| format!("Failed to create directory at {:?}", path))?;
939        }
940        let path = path.to_owned();
941        let contents = content.to_owned();
942        self.executor
943            .spawn(async move {
944                std::fs::write(path, contents)?;
945                Ok(())
946            })
947            .await
948    }
949
950    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
951        let path = path.to_owned();
952        self.executor
953            .spawn(async move {
954                #[cfg(target_os = "windows")]
955                let result = Self::canonicalize(&path);
956
957                #[cfg(not(target_os = "windows"))]
958                let result = std::fs::canonicalize(&path);
959
960                result.with_context(|| format!("canonicalizing {path:?}"))
961            })
962            .await
963    }
964
965    async fn is_file(&self, path: &Path) -> bool {
966        let path = path.to_owned();
967        self.executor
968            .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) })
969            .await
970    }
971
972    async fn is_dir(&self, path: &Path) -> bool {
973        let path = path.to_owned();
974        self.executor
975            .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) })
976            .await
977    }
978
979    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
980        let path_buf = path.to_owned();
981        let symlink_metadata = match self
982            .executor
983            .spawn(async move { std::fs::symlink_metadata(&path_buf) })
984            .await
985        {
986            Ok(metadata) => metadata,
987            Err(err) => {
988                return match err.kind() {
989                    io::ErrorKind::NotFound | io::ErrorKind::NotADirectory => Ok(None),
990                    _ => Err(anyhow::Error::new(err)),
991                };
992            }
993        };
994
995        let is_symlink = symlink_metadata.file_type().is_symlink();
996        let metadata = if is_symlink {
997            let path_buf = path.to_path_buf();
998            // Read target metadata, if the target exists
999            match self
1000                .executor
1001                .spawn(async move { std::fs::metadata(path_buf) })
1002                .await
1003            {
1004                Ok(target_metadata) => target_metadata,
1005                Err(err) => {
1006                    if err.kind() != io::ErrorKind::NotFound {
1007                        // TODO: Also FilesystemLoop when that's stable
1008                        log::warn!(
1009                            "Failed to read symlink target metadata for path {path:?}: {err}"
1010                        );
1011                    }
1012                    // For a broken or recursive symlink, return the symlink metadata. (Or
1013                    // as edge cases, a symlink into a directory we can't read, which is hard
1014                    // to distinguish from just being broken.)
1015                    symlink_metadata
1016                }
1017            }
1018        } else {
1019            symlink_metadata
1020        };
1021
1022        #[cfg(unix)]
1023        let inode = metadata.ino();
1024
1025        #[cfg(windows)]
1026        let inode = file_id(path).await?;
1027
1028        #[cfg(windows)]
1029        let is_fifo = false;
1030
1031        #[cfg(unix)]
1032        let is_fifo = metadata.file_type().is_fifo();
1033
1034        let path_buf = path.to_path_buf();
1035        let is_executable = self
1036            .executor
1037            .spawn(async move { path_buf.is_executable() })
1038            .await;
1039
1040        Ok(Some(Metadata {
1041            inode,
1042            mtime: MTime(metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH)),
1043            len: metadata.len(),
1044            is_symlink,
1045            is_dir: metadata.file_type().is_dir(),
1046            is_fifo,
1047            is_executable,
1048            is_writable: !metadata.permissions().readonly(),
1049        }))
1050    }
1051
1052    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
1053        let path = path.to_owned();
1054        let path = self
1055            .executor
1056            .spawn(async move { std::fs::read_link(&path) })
1057            .await?;
1058        Ok(path)
1059    }
1060
1061    async fn read_dir(
1062        &self,
1063        path: &Path,
1064    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
1065        let path = path.to_owned();
1066        let result = iter(
1067            self.executor
1068                .spawn(async move { std::fs::read_dir(path) })
1069                .await?,
1070        )
1071        .map(|entry| match entry {
1072            Ok(entry) => Ok(entry.path()),
1073            Err(error) => Err(anyhow!("failed to read dir entry {error:?}")),
1074        });
1075        Ok(Box::pin(result))
1076    }
1077
1078    async fn watch(
1079        &self,
1080        path: &Path,
1081        latency: Duration,
1082    ) -> (
1083        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
1084        Arc<dyn Watcher>,
1085    ) {
1086        use util::{ResultExt as _, paths::SanitizedPath};
1087        let executor = self.executor.clone();
1088
1089        let (tx, rx) = async_channel::unbounded();
1090        let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
1091
1092        let watcher: Arc<dyn Watcher> = Arc::new(fs_watcher::FsWatcher::new(
1093            executor.clone(),
1094            tx.clone(),
1095            pending_paths.clone(),
1096        ));
1097
1098        if let Err(e) = watcher.add(path) {
1099            log::warn!("Failed to watch {}:\n{e}", path.display());
1100        }
1101
1102        // Check if path is a symlink and follow the target parent
1103        if let Some(mut target) = self.read_link(path).await.ok() {
1104            log::trace!("watch symlink {path:?} -> {target:?}");
1105            // Check if symlink target is relative path, if so make it absolute
1106            if target.is_relative()
1107                && let Some(parent) = path.parent()
1108            {
1109                target = parent.join(target);
1110                if let Ok(canonical) = self.canonicalize(&target).await {
1111                    target = SanitizedPath::new(&canonical).as_path().to_path_buf();
1112                }
1113            }
1114            watcher.add(&target).ok();
1115            // Skipped for poll watchers: PollWatcher::watch() recursively scans
1116            // at registration, blocking on large virtual filesystem mounts
1117            if let Some(parent) = target.parent()
1118                && !fs_watcher::requires_poll_watcher(parent)
1119            {
1120                watcher.add(parent).log_err();
1121            }
1122        }
1123
1124        (
1125            Box::pin(rx.filter_map({
1126                let watcher = watcher.clone();
1127                let executor = executor.clone();
1128                move |_| {
1129                    let _ = watcher.clone();
1130                    let pending_paths = pending_paths.clone();
1131                    let executor = executor.clone();
1132                    async move {
1133                        executor.timer(latency).await;
1134                        let paths = std::mem::take(&mut *pending_paths.lock());
1135                        log::debug!("pending path events: {:?}", paths);
1136                        (!paths.is_empty()).then_some(paths)
1137                    }
1138                }
1139            })),
1140            watcher,
1141        )
1142    }
1143
1144    fn open_repo(
1145        &self,
1146        dotgit_path: &Path,
1147        system_git_binary_path: Option<&Path>,
1148    ) -> Result<Arc<dyn GitRepository>> {
1149        Ok(Arc::new(RealGitRepository::new(
1150            dotgit_path,
1151            self.bundled_git_binary_path.clone(),
1152            system_git_binary_path.map(|path| path.to_path_buf()),
1153            self.executor.clone(),
1154        )?))
1155    }
1156
1157    async fn git_init(
1158        &self,
1159        abs_work_directory_path: &Path,
1160        fallback_branch_name: String,
1161    ) -> Result<()> {
1162        let result = new_command("git")
1163            .current_dir(abs_work_directory_path)
1164            .args(&["config", "--global", "--get", "init.defaultBranch"])
1165            .output()
1166            .await;
1167
1168        // In case the `git config` command fails, which would be the case if
1169        // the user doesn't have an `init.defaultBranch` value set, we'll just
1170        // default to the provided `fallback_branch_name`.
1171        let branch_name = match result {
1172            Ok(output) if !output.stdout.is_empty() => String::from_utf8(output.stdout)?,
1173            _ => fallback_branch_name,
1174        };
1175
1176        new_command("git")
1177            .current_dir(abs_work_directory_path)
1178            .args(&["init", "-b"])
1179            .arg(branch_name.trim())
1180            .output()
1181            .await?;
1182
1183        Ok(())
1184    }
1185
1186    async fn git_clone(&self, abs_work_directory: &Path, repo_url: &str) -> Result<()> {
1187        let job_id = self.next_job_id.fetch_add(1, Ordering::SeqCst);
1188        let job_info = JobInfo {
1189            id: job_id,
1190            start: Instant::now(),
1191            message: SharedString::from(format!("Cloning {}", repo_url)),
1192        };
1193
1194        let _job_tracker = JobTracker::new(job_info, self.job_event_subscribers.clone());
1195
1196        let output = new_command("git")
1197            .current_dir(abs_work_directory)
1198            .args(&["clone", repo_url])
1199            .output()
1200            .await?;
1201
1202        if !output.status.success() {
1203            anyhow::bail!(
1204                "git clone failed: {}",
1205                String::from_utf8_lossy(&output.stderr)
1206            );
1207        }
1208
1209        Ok(())
1210    }
1211
1212    /// Runs `git config` with the given arguments.
1213    /// Will return `Ok` if the commands exit status is `0`, with the stdout
1214    /// contents. Otherwise returns `Err` with the stderr contents.
1215    async fn git_config(&self, abs_work_directory: &Path, args: Vec<String>) -> Result<String> {
1216        let output = new_command("git")
1217            .current_dir(abs_work_directory)
1218            .args([String::from("config")].into_iter().chain(args))
1219            .output()
1220            .await?;
1221
1222        if !output.status.success() {
1223            let err = String::from_utf8(output.stderr)?;
1224            anyhow::bail!(err);
1225        }
1226
1227        String::from_utf8(output.stdout).map_err(Into::into)
1228    }
1229
1230    fn is_fake(&self) -> bool {
1231        false
1232    }
1233
1234    fn subscribe_to_jobs(&self) -> JobEventReceiver {
1235        let (sender, receiver) = futures::channel::mpsc::unbounded();
1236        self.job_event_subscribers.lock().push(sender);
1237        receiver
1238    }
1239
1240    /// Checks whether the file system is case sensitive by attempting to create two files
1241    /// that have the same name except for the casing.
1242    ///
1243    /// It creates both files in a temporary directory it removes at the end.
1244    async fn is_case_sensitive(&self) -> bool {
1245        const UNINITIALIZED: u8 = 0;
1246        const CASE_SENSITIVE: u8 = 1;
1247        const NOT_CASE_SENSITIVE: u8 = 2;
1248
1249        // Note we could CAS here, but really, if we race we do this work twice at worst which isn't a big deal.
1250        let load = self.is_case_sensitive.load(Ordering::Acquire);
1251        if load != UNINITIALIZED {
1252            return load == CASE_SENSITIVE;
1253        }
1254        let temp_dir = self.executor.spawn(async { TempDir::new() });
1255        let res = maybe!(async {
1256            let temp_dir = temp_dir.await?;
1257            let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
1258            let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
1259
1260            let create_opts = CreateOptions {
1261                overwrite: false,
1262                ignore_if_exists: false,
1263            };
1264
1265            // Create file1
1266            self.create_file(&test_file_1, create_opts).await?;
1267
1268            // Now check whether it's possible to create file2
1269            let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
1270                Ok(_) => Ok(true),
1271                Err(e) => {
1272                    if let Some(io_error) = e.downcast_ref::<io::Error>() {
1273                        if io_error.kind() == io::ErrorKind::AlreadyExists {
1274                            Ok(false)
1275                        } else {
1276                            Err(e)
1277                        }
1278                    } else {
1279                        Err(e)
1280                    }
1281                }
1282            };
1283
1284            temp_dir.close()?;
1285            case_sensitive
1286        }).await.unwrap_or_else(|e| {
1287            log::error!(
1288                "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}"
1289            );
1290            true
1291        });
1292        self.is_case_sensitive.store(
1293            if res {
1294                CASE_SENSITIVE
1295            } else {
1296                NOT_CASE_SENSITIVE
1297            },
1298            Ordering::Release,
1299        );
1300        res
1301    }
1302
1303    async fn restore(&self, item: TrashId) -> std::result::Result<PathBuf, TrashRestoreError> {
1304        let trashed_entry = self
1305            .trash
1306            .lock()
1307            .remove(item)
1308            .ok_or(TrashRestoreError::AlreadyRestored)?;
1309
1310        let restored_item_path = trashed_entry.original_parent.join(&trashed_entry.name);
1311
1312        let (tx, rx) = futures::channel::oneshot::channel();
1313        std::thread::Builder::new()
1314            .name("restore trashed item".to_string())
1315            .spawn(move || {
1316                let res = trash::restore_all([trashed_entry.into_trash_item()]);
1317                tx.send(res)
1318            })
1319            .expect("The OS can spawn a threads");
1320        rx.await.expect("Restore all never panics")?;
1321        Ok(restored_item_path)
1322    }
1323}
1324
1325#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
1326impl Watcher for RealWatcher {
1327    fn add(&self, _: &Path) -> Result<()> {
1328        Ok(())
1329    }
1330
1331    fn remove(&self, _: &Path) -> Result<()> {
1332        Ok(())
1333    }
1334}
1335
1336#[cfg(feature = "test-support")]
1337pub struct FakeFs {
1338    this: std::sync::Weak<Self>,
1339    // Use an unfair lock to ensure tests are deterministic.
1340    state: Arc<Mutex<FakeFsState>>,
1341    executor: gpui::BackgroundExecutor,
1342}
1343
1344#[cfg(feature = "test-support")]
1345struct FakeFsState {
1346    root: FakeFsEntry,
1347    next_inode: u64,
1348    next_mtime: SystemTime,
1349    git_event_tx: async_channel::Sender<PathBuf>,
1350    event_txs: Vec<(PathBuf, async_channel::Sender<Vec<PathEvent>>)>,
1351    events_paused: bool,
1352    buffered_events: Vec<PathEvent>,
1353    metadata_call_count: usize,
1354    read_dir_call_count: usize,
1355    path_write_counts: std::collections::HashMap<PathBuf, usize>,
1356    moves: std::collections::HashMap<u64, PathBuf>,
1357    job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>,
1358    trash: Mutex<SlotMap<TrashId, (TrashedEntry, FakeFsEntry)>>,
1359    file_to_create_before_watch_add: Option<(PathBuf, PathBuf)>,
1360    remove_dir_errors: std::collections::HashMap<PathBuf, String>,
1361}
1362
1363#[cfg(feature = "test-support")]
1364impl FakeFsState {
1365    fn create_file_before_watch_add(&mut self, watch_path: &Path) -> Result<()> {
1366        let Some((pending_watch_path, file_path)) = self.file_to_create_before_watch_add.take()
1367        else {
1368            return Ok(());
1369        };
1370        if pending_watch_path != watch_path {
1371            self.file_to_create_before_watch_add = Some((pending_watch_path, file_path));
1372            return Ok(());
1373        }
1374
1375        let inode = self.get_and_increment_inode();
1376        let mtime = self.get_and_increment_mtime();
1377        self.write_path(&file_path, |entry| {
1378            let btree_map::Entry::Vacant(entry) = entry else {
1379                anyhow::bail!("file already exists: {}", file_path.display());
1380            };
1381            entry.insert(FakeFsEntry::File {
1382                inode,
1383                mtime,
1384                len: 0,
1385                content: Vec::new(),
1386                git_dir_path: None,
1387            });
1388            Ok(())
1389        })?;
1390        self.emit_event([(file_path, Some(PathEventKind::Created))]);
1391        Ok(())
1392    }
1393}
1394
1395#[cfg(feature = "test-support")]
1396#[derive(Clone, Debug)]
1397enum FakeFsEntry {
1398    File {
1399        inode: u64,
1400        mtime: MTime,
1401        len: u64,
1402        content: Vec<u8>,
1403        // The path to the repository state directory, if this is a gitfile.
1404        git_dir_path: Option<PathBuf>,
1405    },
1406    Dir {
1407        inode: u64,
1408        mtime: MTime,
1409        len: u64,
1410        entries: BTreeMap<String, FakeFsEntry>,
1411        git_repo_state: Option<Arc<Mutex<FakeGitRepositoryState>>>,
1412    },
1413    Symlink {
1414        target: PathBuf,
1415    },
1416}
1417
1418#[cfg(feature = "test-support")]
1419impl PartialEq for FakeFsEntry {
1420    fn eq(&self, other: &Self) -> bool {
1421        match (self, other) {
1422            (
1423                Self::File {
1424                    inode: l_inode,
1425                    mtime: l_mtime,
1426                    len: l_len,
1427                    content: l_content,
1428                    git_dir_path: l_git_dir_path,
1429                },
1430                Self::File {
1431                    inode: r_inode,
1432                    mtime: r_mtime,
1433                    len: r_len,
1434                    content: r_content,
1435                    git_dir_path: r_git_dir_path,
1436                },
1437            ) => {
1438                l_inode == r_inode
1439                    && l_mtime == r_mtime
1440                    && l_len == r_len
1441                    && l_content == r_content
1442                    && l_git_dir_path == r_git_dir_path
1443            }
1444            (
1445                Self::Dir {
1446                    inode: l_inode,
1447                    mtime: l_mtime,
1448                    len: l_len,
1449                    entries: l_entries,
1450                    git_repo_state: l_git_repo_state,
1451                },
1452                Self::Dir {
1453                    inode: r_inode,
1454                    mtime: r_mtime,
1455                    len: r_len,
1456                    entries: r_entries,
1457                    git_repo_state: r_git_repo_state,
1458                },
1459            ) => {
1460                let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) {
1461                    (Some(l), Some(r)) => Arc::ptr_eq(l, r),
1462                    (None, None) => true,
1463                    _ => false,
1464                };
1465                l_inode == r_inode
1466                    && l_mtime == r_mtime
1467                    && l_len == r_len
1468                    && l_entries == r_entries
1469                    && same_repo_state
1470            }
1471            (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => {
1472                l_target == r_target
1473            }
1474            _ => false,
1475        }
1476    }
1477}
1478
1479#[cfg(feature = "test-support")]
1480impl FakeFsState {
1481    fn get_and_increment_mtime(&mut self) -> MTime {
1482        let mtime = self.next_mtime;
1483        self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
1484        MTime(mtime)
1485    }
1486
1487    fn get_and_increment_inode(&mut self) -> u64 {
1488        let inode = self.next_inode;
1489        self.next_inode += 1;
1490        inode
1491    }
1492
1493    fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option<PathBuf> {
1494        let mut canonical_path = PathBuf::new();
1495        let mut path = target.to_path_buf();
1496        let mut entry_stack = Vec::new();
1497        'outer: loop {
1498            let mut path_components = path.components().peekable();
1499            let mut prefix = None;
1500            while let Some(component) = path_components.next() {
1501                match component {
1502                    Component::Prefix(prefix_component) => prefix = Some(prefix_component),
1503                    Component::RootDir => {
1504                        entry_stack.clear();
1505                        entry_stack.push(&self.root);
1506                        canonical_path.clear();
1507                        match prefix {
1508                            Some(prefix_component) => {
1509                                canonical_path = PathBuf::from(prefix_component.as_os_str());
1510                                // Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
1511                                canonical_path.push(std::path::MAIN_SEPARATOR_STR);
1512                            }
1513                            None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
1514                        }
1515                    }
1516                    Component::CurDir => {}
1517                    Component::ParentDir => {
1518                        entry_stack.pop()?;
1519                        canonical_path.pop();
1520                    }
1521                    Component::Normal(name) => {
1522                        let current_entry = *entry_stack.last()?;
1523                        if let FakeFsEntry::Dir { entries, .. } = current_entry {
1524                            let entry = entries.get(name.to_str().unwrap())?;
1525                            if (path_components.peek().is_some() || follow_symlink)
1526                                && let FakeFsEntry::Symlink { target, .. } = entry
1527                            {
1528                                let mut target = target.clone();
1529                                target.extend(path_components);
1530                                path = target;
1531                                continue 'outer;
1532                            }
1533                            entry_stack.push(entry);
1534                            canonical_path = canonical_path.join(name);
1535                        } else {
1536                            return None;
1537                        }
1538                    }
1539                }
1540            }
1541            break;
1542        }
1543
1544        if entry_stack.is_empty() {
1545            None
1546        } else {
1547            Some(canonical_path)
1548        }
1549    }
1550
1551    fn try_entry(
1552        &mut self,
1553        target: &Path,
1554        follow_symlink: bool,
1555    ) -> Option<(&mut FakeFsEntry, PathBuf)> {
1556        let canonical_path = self.canonicalize(target, follow_symlink)?;
1557
1558        let mut components = canonical_path
1559            .components()
1560            .skip_while(|component| matches!(component, Component::Prefix(_)));
1561        let Some(Component::RootDir) = components.next() else {
1562            panic!(
1563                "the path {:?} was not canonicalized properly {:?}",
1564                target, canonical_path
1565            )
1566        };
1567
1568        let mut entry = &mut self.root;
1569        for component in components {
1570            match component {
1571                Component::Normal(name) => {
1572                    if let FakeFsEntry::Dir { entries, .. } = entry {
1573                        entry = entries.get_mut(name.to_str().unwrap())?;
1574                    } else {
1575                        return None;
1576                    }
1577                }
1578                _ => {
1579                    panic!(
1580                        "the path {:?} was not canonicalized properly {:?}",
1581                        target, canonical_path
1582                    )
1583                }
1584            }
1585        }
1586
1587        Some((entry, canonical_path))
1588    }
1589
1590    fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> {
1591        Ok(self
1592            .try_entry(target, true)
1593            .ok_or_else(|| {
1594                anyhow!(io::Error::new(
1595                    io::ErrorKind::NotFound,
1596                    format!("not found: {target:?}")
1597                ))
1598            })?
1599            .0)
1600    }
1601
1602    fn write_path<Fn, T>(&mut self, path: &Path, callback: Fn) -> Result<T>
1603    where
1604        Fn: FnOnce(btree_map::Entry<String, FakeFsEntry>) -> Result<T>,
1605    {
1606        let path = normalize_path(path);
1607        let filename = path.file_name().context("cannot overwrite the root")?;
1608        let parent_path = path.parent().unwrap();
1609
1610        let parent = self.entry(parent_path)?;
1611        let new_entry = parent
1612            .dir_entries(parent_path)?
1613            .entry(filename.to_str().unwrap().into());
1614        callback(new_entry)
1615    }
1616
1617    fn emit_event<I, T>(&mut self, paths: I)
1618    where
1619        I: IntoIterator<Item = (T, Option<PathEventKind>)>,
1620        T: Into<PathBuf>,
1621    {
1622        self.buffered_events
1623            .extend(paths.into_iter().map(|(path, kind)| PathEvent {
1624                path: path.into(),
1625                kind,
1626            }));
1627
1628        if !self.events_paused {
1629            self.flush_events(self.buffered_events.len());
1630        }
1631    }
1632
1633    fn flush_events(&mut self, mut count: usize) {
1634        count = count.min(self.buffered_events.len());
1635        let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
1636        self.event_txs.retain(|(_, tx)| {
1637            let _ = tx.try_send(events.clone());
1638            !tx.is_closed()
1639        });
1640    }
1641}
1642
1643#[cfg(feature = "test-support")]
1644pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
1645    std::sync::LazyLock::new(|| OsStr::new(".git"));
1646
1647#[cfg(feature = "test-support")]
1648impl FakeFs {
1649    /// We need to use something large enough for Windows and Unix to consider this a new file.
1650    /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
1651    const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
1652
1653    pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
1654        let (tx, rx) = async_channel::bounded::<PathBuf>(10);
1655
1656        let this = Arc::new_cyclic(|this| Self {
1657            this: this.clone(),
1658            executor: executor.clone(),
1659            state: Arc::new(Mutex::new(FakeFsState {
1660                root: FakeFsEntry::Dir {
1661                    inode: 0,
1662                    mtime: MTime(UNIX_EPOCH),
1663                    len: 0,
1664                    entries: Default::default(),
1665                    git_repo_state: None,
1666                },
1667                git_event_tx: tx,
1668                next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
1669                next_inode: 1,
1670                event_txs: Default::default(),
1671                buffered_events: Vec::new(),
1672                events_paused: false,
1673                read_dir_call_count: 0,
1674                metadata_call_count: 0,
1675                path_write_counts: Default::default(),
1676                moves: Default::default(),
1677                job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
1678                trash: Mutex::new(SlotMap::with_key()),
1679                file_to_create_before_watch_add: None,
1680                remove_dir_errors: Default::default(),
1681            })),
1682        });
1683
1684        executor.spawn({
1685            let this = this.clone();
1686            async move {
1687                while let Ok(git_event) = rx.recv().await {
1688                    if let Some(mut state) = this.state.try_lock() {
1689                        state.emit_event([(git_event, Some(PathEventKind::Changed))]);
1690                    } else {
1691                        panic!("Failed to lock file system state, this execution would have caused a test hang");
1692                    }
1693                }
1694            }
1695        }).detach();
1696
1697        this
1698    }
1699
1700    pub fn set_next_mtime(&self, next_mtime: SystemTime) {
1701        let mut state = self.state.lock();
1702        state.next_mtime = next_mtime;
1703    }
1704
1705    pub fn get_and_increment_mtime(&self) -> MTime {
1706        let mut state = self.state.lock();
1707        state.get_and_increment_mtime()
1708    }
1709
1710    pub async fn touch_path(&self, path: impl AsRef<Path>) {
1711        let mut state = self.state.lock();
1712        let path = path.as_ref();
1713        let new_mtime = state.get_and_increment_mtime();
1714        let new_inode = state.get_and_increment_inode();
1715        state
1716            .write_path(path, move |entry| {
1717                match entry {
1718                    btree_map::Entry::Vacant(e) => {
1719                        e.insert(FakeFsEntry::File {
1720                            inode: new_inode,
1721                            mtime: new_mtime,
1722                            content: Vec::new(),
1723                            len: 0,
1724                            git_dir_path: None,
1725                        });
1726                    }
1727                    btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() {
1728                        FakeFsEntry::File { mtime, .. } => *mtime = new_mtime,
1729                        FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime,
1730                        FakeFsEntry::Symlink { .. } => {}
1731                    },
1732                }
1733                Ok(())
1734            })
1735            .unwrap();
1736        state.emit_event([(path.to_path_buf(), Some(PathEventKind::Changed))]);
1737    }
1738
1739    pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
1740        self.write_file_internal(path, content, true).unwrap()
1741    }
1742
1743    pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
1744        let mut state = self.state.lock();
1745        let path = path.as_ref();
1746        let file = FakeFsEntry::Symlink { target };
1747        state
1748            .write_path(path.as_ref(), move |e| match e {
1749                btree_map::Entry::Vacant(e) => {
1750                    e.insert(file);
1751                    Ok(())
1752                }
1753                btree_map::Entry::Occupied(mut e) => {
1754                    *e.get_mut() = file;
1755                    Ok(())
1756                }
1757            })
1758            .unwrap();
1759        state.emit_event([(path, Some(PathEventKind::Created))]);
1760    }
1761
1762    fn write_file_internal(
1763        &self,
1764        path: impl AsRef<Path>,
1765        new_content: Vec<u8>,
1766        recreate_inode: bool,
1767    ) -> Result<()> {
1768        fn inner(
1769            this: &FakeFs,
1770            path: &Path,
1771            new_content: Vec<u8>,
1772            recreate_inode: bool,
1773        ) -> Result<()> {
1774            let mut state = this.state.lock();
1775            let path_buf = path.to_path_buf();
1776            *state.path_write_counts.entry(path_buf).or_insert(0) += 1;
1777            let new_inode = state.get_and_increment_inode();
1778            let new_mtime = state.get_and_increment_mtime();
1779            let new_len = new_content.len() as u64;
1780            let mut kind = None;
1781            state.write_path(path, |entry| {
1782                match entry {
1783                    btree_map::Entry::Vacant(e) => {
1784                        kind = Some(PathEventKind::Created);
1785                        e.insert(FakeFsEntry::File {
1786                            inode: new_inode,
1787                            mtime: new_mtime,
1788                            len: new_len,
1789                            content: new_content,
1790                            git_dir_path: None,
1791                        });
1792                    }
1793                    btree_map::Entry::Occupied(mut e) => {
1794                        kind = Some(PathEventKind::Changed);
1795                        if let FakeFsEntry::File {
1796                            inode,
1797                            mtime,
1798                            len,
1799                            content,
1800                            ..
1801                        } = e.get_mut()
1802                        {
1803                            *mtime = new_mtime;
1804                            *content = new_content;
1805                            *len = new_len;
1806                            if recreate_inode {
1807                                *inode = new_inode;
1808                            }
1809                        } else {
1810                            anyhow::bail!("not a file")
1811                        }
1812                    }
1813                }
1814                Ok(())
1815            })?;
1816            state.emit_event([(path, kind)]);
1817            Ok(())
1818        }
1819        inner(self, path.as_ref(), new_content, recreate_inode)
1820    }
1821
1822    pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1823        let path = path.as_ref();
1824        let path = normalize_path(path);
1825        let mut state = self.state.lock();
1826        let entry = state.entry(&path)?;
1827        entry.file_content(&path).cloned()
1828    }
1829
1830    async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1831        let path = path.as_ref();
1832        let path = normalize_path(path);
1833        self.simulate_random_delay().await;
1834        let mut state = self.state.lock();
1835        let entry = state.entry(&path)?;
1836        entry.file_content(&path).cloned()
1837    }
1838
1839    pub fn pause_events(&self) {
1840        self.state.lock().events_paused = true;
1841    }
1842
1843    pub fn unpause_events_and_flush(&self) {
1844        self.state.lock().events_paused = false;
1845        self.flush_events(usize::MAX);
1846    }
1847
1848    pub fn buffered_event_count(&self) -> usize {
1849        self.state.lock().buffered_events.len()
1850    }
1851
1852    pub fn clear_buffered_events(&self) {
1853        self.state.lock().buffered_events.clear();
1854    }
1855
1856    /// Simulates the kernel's watch queue overflowing: all buffered
1857    /// (undelivered) events are lost, and the watcher reports only a single
1858    /// `Rescan` event for `root`, mirroring how the native backends report
1859    /// lost sync (FSEvents `kFSEventStreamEventFlagMustScanSubDirs`, inotify
1860    /// `IN_Q_OVERFLOW`, Windows `ERROR_NOTIFY_ENUM_DIR`).
1861    ///
1862    /// Note that the fake file system's state is unaffected; like a real
1863    /// overflow, only the notifications are lost, not the changes themselves.
1864    pub fn simulate_watcher_overflow(&self, root: impl Into<PathBuf>) {
1865        let mut state = self.state.lock();
1866        state.buffered_events.clear();
1867        state.emit_event([(root, Some(PathEventKind::Rescan))]);
1868    }
1869
1870    pub fn create_file_before_next_watch_add(
1871        &self,
1872        watch_path: impl AsRef<Path>,
1873        path: impl AsRef<Path>,
1874    ) {
1875        self.state.lock().file_to_create_before_watch_add = Some((
1876            normalize_path(watch_path.as_ref()),
1877            normalize_path(path.as_ref()),
1878        ));
1879    }
1880
1881    pub fn flush_events(&self, count: usize) {
1882        self.state.lock().flush_events(count);
1883    }
1884
1885    pub(crate) fn entry(&self, target: &Path) -> Result<FakeFsEntry> {
1886        self.state.lock().entry(target).cloned()
1887    }
1888
1889    pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> {
1890        let mut state = self.state.lock();
1891        state.write_path(target, |entry| {
1892            match entry {
1893                btree_map::Entry::Vacant(vacant_entry) => {
1894                    vacant_entry.insert(new_entry);
1895                }
1896                btree_map::Entry::Occupied(mut occupied_entry) => {
1897                    occupied_entry.insert(new_entry);
1898                }
1899            }
1900            Ok(())
1901        })
1902    }
1903
1904    #[must_use]
1905    pub fn insert_tree<'a>(
1906        &'a self,
1907        path: impl 'a + AsRef<Path> + Send,
1908        tree: serde_json::Value,
1909    ) -> futures::future::BoxFuture<'a, ()> {
1910        use futures::FutureExt as _;
1911        use serde_json::Value::*;
1912
1913        fn inner<'a>(
1914            this: &'a FakeFs,
1915            path: Arc<Path>,
1916            tree: serde_json::Value,
1917        ) -> futures::future::BoxFuture<'a, ()> {
1918            async move {
1919                match tree {
1920                    Object(map) => {
1921                        this.create_dir(&path).await.unwrap();
1922                        for (name, contents) in map {
1923                            let mut path = PathBuf::from(path.as_ref());
1924                            path.push(name);
1925                            this.insert_tree(&path, contents).await;
1926                        }
1927                    }
1928                    Null => {
1929                        this.create_dir(&path).await.unwrap();
1930                    }
1931                    String(contents) => {
1932                        this.insert_file(&path, contents.into_bytes()).await;
1933                    }
1934                    _ => {
1935                        panic!("JSON object must contain only objects, strings, or null");
1936                    }
1937                }
1938            }
1939            .boxed()
1940        }
1941        inner(self, Arc::from(path.as_ref()), tree)
1942    }
1943
1944    pub fn insert_tree_from_real_fs<'a>(
1945        &'a self,
1946        path: impl 'a + AsRef<Path> + Send,
1947        src_path: impl 'a + AsRef<Path> + Send,
1948    ) -> futures::future::BoxFuture<'a, ()> {
1949        use futures::FutureExt as _;
1950
1951        async move {
1952            let path = path.as_ref();
1953            if std::fs::metadata(&src_path).unwrap().is_file() {
1954                let contents = std::fs::read(src_path).unwrap();
1955                self.insert_file(path, contents).await;
1956            } else {
1957                self.create_dir(path).await.unwrap();
1958                for entry in std::fs::read_dir(&src_path).unwrap() {
1959                    let entry = entry.unwrap();
1960                    self.insert_tree_from_real_fs(path.join(entry.file_name()), entry.path())
1961                        .await;
1962                }
1963            }
1964        }
1965        .boxed()
1966    }
1967
1968    pub fn with_git_state_and_paths<T, F>(
1969        &self,
1970        dot_git: &Path,
1971        emit_git_event: bool,
1972        f: F,
1973    ) -> Result<T>
1974    where
1975        F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T,
1976    {
1977        let mut state = self.state.lock();
1978        let git_event_tx = state.git_event_tx.clone();
1979        let entry = state.entry(dot_git).context("open .git")?;
1980
1981        if let FakeFsEntry::Dir { git_repo_state, .. } = entry {
1982            let repo_state = git_repo_state.get_or_insert_with(|| {
1983                log::debug!("insert git state for {dot_git:?}");
1984                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
1985            });
1986            let mut repo_state = repo_state.lock();
1987
1988            let result = f(&mut repo_state, dot_git, dot_git);
1989
1990            drop(repo_state);
1991            if emit_git_event {
1992                state.emit_event([(
1993                    dot_git.join("fake_git_repo_event"),
1994                    Some(PathEventKind::Changed),
1995                )]);
1996            }
1997
1998            Ok(result)
1999        } else if let FakeFsEntry::File {
2000            content,
2001            git_dir_path,
2002            ..
2003        } = &mut *entry
2004        {
2005            let path = match git_dir_path {
2006                Some(path) => path,
2007                None => {
2008                    let path = std::str::from_utf8(content)
2009                        .ok()
2010                        .and_then(|content| content.strip_prefix("gitdir:"))
2011                        .context("not a valid gitfile")?
2012                        .trim();
2013                    git_dir_path.insert(normalize_path(&dot_git.parent().unwrap().join(path)))
2014                }
2015            }
2016            .clone();
2017            let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else {
2018                anyhow::bail!("pointed-to git dir {path:?} not found")
2019            };
2020            let FakeFsEntry::Dir {
2021                git_repo_state,
2022                entries,
2023                ..
2024            } = git_dir_entry
2025            else {
2026                anyhow::bail!("gitfile points to a non-directory")
2027            };
2028            let common_dir = if let Some(child) = entries.get("commondir") {
2029                let raw = std::str::from_utf8(child.file_content("commondir".as_ref())?)
2030                    .context("commondir content")?
2031                    .trim();
2032                let raw_path = Path::new(raw);
2033                if raw_path.is_relative() {
2034                    normalize_path(&canonical_path.join(raw_path))
2035                } else {
2036                    raw_path.to_owned()
2037                }
2038            } else {
2039                canonical_path.clone()
2040            };
2041            let repo_state = git_repo_state.get_or_insert_with(|| {
2042                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
2043            });
2044            let mut repo_state = repo_state.lock();
2045
2046            let result = f(&mut repo_state, &canonical_path, &common_dir);
2047
2048            if emit_git_event {
2049                drop(repo_state);
2050                state.emit_event([(
2051                    canonical_path.join("fake_git_repo_event"),
2052                    Some(PathEventKind::Changed),
2053                )]);
2054            }
2055
2056            Ok(result)
2057        } else {
2058            anyhow::bail!("not a valid git repository");
2059        }
2060    }
2061
2062    pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
2063    where
2064        F: FnOnce(&mut FakeGitRepositoryState) -> T,
2065    {
2066        self.with_git_state_and_paths(dot_git, emit_git_event, |state, _, _| f(state))
2067    }
2068
2069    pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
2070        self.with_git_state(dot_git, true, |state| {
2071            let branch = branch.map(Into::into);
2072            state.branches.extend(branch.clone());
2073            state.current_branch_name = branch
2074        })
2075        .unwrap();
2076    }
2077
2078    pub fn set_remote_for_repo(
2079        &self,
2080        dot_git: &Path,
2081        name: impl Into<String>,
2082        url: impl Into<String>,
2083    ) {
2084        self.with_git_state(dot_git, true, |state| {
2085            state.remotes.insert(name.into(), url.into());
2086        })
2087        .unwrap();
2088    }
2089
2090    pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
2091        self.with_git_state(dot_git, true, |state| {
2092            if let Some(first) = branches.first()
2093                && state.current_branch_name.is_none()
2094            {
2095                state.current_branch_name = Some(first.to_string())
2096            }
2097            state
2098                .branches
2099                .extend(branches.iter().map(ToString::to_string));
2100        })
2101        .unwrap();
2102    }
2103
2104    pub async fn add_linked_worktree_for_repo(
2105        &self,
2106        dot_git: &Path,
2107        emit_git_event: bool,
2108        worktree: Worktree,
2109    ) {
2110        let ref_name = worktree
2111            .ref_name
2112            .as_ref()
2113            .expect("linked worktree must have a ref_name");
2114        let branch_name = ref_name
2115            .strip_prefix("refs/heads/")
2116            .unwrap_or(ref_name.as_ref());
2117
2118        // Create ref in git state.
2119        self.with_git_state(dot_git, false, |state| {
2120            state
2121                .refs
2122                .insert(ref_name.to_string(), worktree.sha.to_string());
2123        })
2124        .unwrap();
2125
2126        // Create .git/worktrees/<name>/ directory with HEAD, commondir, and gitdir.
2127        let worktrees_entry_dir = dot_git.join("worktrees").join(branch_name);
2128        self.create_dir(&worktrees_entry_dir).await.unwrap();
2129
2130        self.write_file_internal(
2131            worktrees_entry_dir.join("HEAD"),
2132            format!("ref: {ref_name}").into_bytes(),
2133            false,
2134        )
2135        .unwrap();
2136
2137        self.write_file_internal(
2138            worktrees_entry_dir.join("commondir"),
2139            dot_git.to_string_lossy().into_owned().into_bytes(),
2140            false,
2141        )
2142        .unwrap();
2143
2144        let worktree_dot_git = worktree.path.join(".git");
2145        self.write_file_internal(
2146            worktrees_entry_dir.join("gitdir"),
2147            worktree_dot_git.to_string_lossy().into_owned().into_bytes(),
2148            false,
2149        )
2150        .unwrap();
2151
2152        // Create the worktree checkout directory with a .git file pointing back.
2153        self.create_dir(&worktree.path).await.unwrap();
2154
2155        self.write_file_internal(
2156            &worktree_dot_git,
2157            format!("gitdir: {}", worktrees_entry_dir.display()).into_bytes(),
2158            false,
2159        )
2160        .unwrap();
2161
2162        if emit_git_event {
2163            self.with_git_state(dot_git, true, |_| {}).unwrap();
2164        }
2165    }
2166
2167    pub async fn remove_worktree_for_repo(
2168        &self,
2169        dot_git: &Path,
2170        emit_git_event: bool,
2171        ref_name: &str,
2172    ) {
2173        let branch_name = ref_name.strip_prefix("refs/heads/").unwrap_or(ref_name);
2174        let worktrees_entry_dir = dot_git.join("worktrees").join(branch_name);
2175
2176        // Read gitdir to find the worktree checkout path.
2177        let gitdir_content = self
2178            .load_internal(worktrees_entry_dir.join("gitdir"))
2179            .await
2180            .unwrap();
2181        let gitdir_str = String::from_utf8(gitdir_content).unwrap();
2182        let worktree_path = PathBuf::from(gitdir_str.trim())
2183            .parent()
2184            .map(PathBuf::from)
2185            .unwrap_or_default();
2186
2187        // Remove the worktree checkout directory.
2188        self.remove_dir(
2189            &worktree_path,
2190            RemoveOptions {
2191                recursive: true,
2192                ignore_if_not_exists: true,
2193            },
2194        )
2195        .await
2196        .unwrap();
2197
2198        // Remove the .git/worktrees/<name>/ directory.
2199        self.remove_dir(
2200            &worktrees_entry_dir,
2201            RemoveOptions {
2202                recursive: true,
2203                ignore_if_not_exists: false,
2204            },
2205        )
2206        .await
2207        .unwrap();
2208
2209        if emit_git_event {
2210            self.with_git_state(dot_git, true, |_| {}).unwrap();
2211        }
2212    }
2213
2214    pub fn set_unmerged_paths_for_repo(
2215        &self,
2216        dot_git: &Path,
2217        unmerged_state: &[(RepoPath, UnmergedStatus)],
2218    ) {
2219        self.with_git_state(dot_git, true, |state| {
2220            state.unmerged_paths.clear();
2221            state.unmerged_paths.extend(
2222                unmerged_state
2223                    .iter()
2224                    .map(|(path, content)| (path.clone(), *content)),
2225            );
2226        })
2227        .unwrap();
2228    }
2229
2230    pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(&str, String)]) {
2231        self.with_git_state(dot_git, true, |state| {
2232            state.index_contents.clear();
2233            state.index_contents.extend(
2234                index_state
2235                    .iter()
2236                    .map(|(path, content)| (repo_path(path), content.clone())),
2237            );
2238        })
2239        .unwrap();
2240    }
2241
2242    pub fn set_head_for_repo(
2243        &self,
2244        dot_git: &Path,
2245        head_state: &[(&str, String)],
2246        sha: impl Into<String>,
2247    ) {
2248        self.with_git_state(dot_git, true, |state| {
2249            state.head_contents.clear();
2250            state.head_contents.extend(
2251                head_state
2252                    .iter()
2253                    .map(|(path, content)| (repo_path(path), content.clone())),
2254            );
2255            state.refs.insert("HEAD".into(), sha.into());
2256        })
2257        .unwrap();
2258    }
2259
2260    pub fn set_head_and_index_for_repo(&self, dot_git: &Path, contents_by_path: &[(&str, String)]) {
2261        self.with_git_state(dot_git, true, |state| {
2262            state.head_contents.clear();
2263            state.head_contents.extend(
2264                contents_by_path
2265                    .iter()
2266                    .map(|(path, contents)| (repo_path(path), contents.clone())),
2267            );
2268            state.index_contents = state.head_contents.clone();
2269        })
2270        .unwrap();
2271    }
2272
2273    pub fn set_merge_base_content_for_repo(
2274        &self,
2275        dot_git: &Path,
2276        contents_by_path: &[(&str, String)],
2277    ) {
2278        self.with_git_state(dot_git, true, |state| {
2279            use git::Oid;
2280
2281            state.merge_base_contents.clear();
2282            let oids = (1..)
2283                .map(|n| n.to_string())
2284                .map(|n| Oid::from_bytes(n.repeat(20).as_bytes()).unwrap());
2285            for ((path, content), oid) in contents_by_path.iter().zip(oids) {
2286                state.merge_base_contents.insert(repo_path(path), oid);
2287                state.oids.insert(oid, content.clone());
2288            }
2289        })
2290        .unwrap();
2291    }
2292
2293    pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
2294        self.with_git_state(dot_git, true, |state| {
2295            state.blames.clear();
2296            state.blames.extend(blames);
2297        })
2298        .unwrap();
2299    }
2300
2301    pub fn set_graph_commits(&self, dot_git: &Path, commits: Vec<Arc<InitialGraphCommitData>>) {
2302        self.with_git_state(dot_git, true, |state| {
2303            state.graph_commits = commits;
2304        })
2305        .unwrap();
2306    }
2307
2308    pub fn set_graph_error(&self, dot_git: &Path, error: Option<String>) {
2309        self.with_git_state(dot_git, true, |state| {
2310            state.simulated_graph_error = error;
2311        })
2312        .unwrap();
2313    }
2314
2315    pub fn set_commit_data(
2316        &self,
2317        dot_git: &Path,
2318        commit_data: impl IntoIterator<Item = (CommitData, bool)>,
2319    ) {
2320        self.with_git_state(dot_git, true, |state| {
2321            state.commit_data = commit_data
2322                .into_iter()
2323                .map(|(data, should_fail)| {
2324                    (
2325                        data.sha,
2326                        if should_fail {
2327                            FakeCommitDataEntry::Fail(data)
2328                        } else {
2329                            FakeCommitDataEntry::Success(data)
2330                        },
2331                    )
2332                })
2333                .collect();
2334        })
2335        .unwrap();
2336    }
2337
2338    /// Put the given git repository into a state with the given status,
2339    /// by mutating the head, index, and unmerged state.
2340    pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&str, FileStatus)]) {
2341        let workdir_path = dot_git.parent().unwrap();
2342        let workdir_contents = self.files_with_contents(workdir_path);
2343        self.with_git_state(dot_git, true, |state| {
2344            state.index_contents.clear();
2345            state.head_contents.clear();
2346            state.unmerged_paths.clear();
2347            for (path, content) in workdir_contents {
2348                use util::{paths::PathStyle, rel_path::RelPath};
2349
2350                let repo_path = RelPath::new(path.strip_prefix(&workdir_path).unwrap(), PathStyle::local()).unwrap();
2351                let repo_path = RepoPath::from_rel_path(&repo_path);
2352                let status = statuses
2353                    .iter()
2354                    .find_map(|(p, status)| (*p == repo_path.as_unix_str()).then_some(status));
2355                let mut content = String::from_utf8_lossy(&content).to_string();
2356
2357                let mut index_content = None;
2358                let mut head_content = None;
2359                match status {
2360                    None => {
2361                        index_content = Some(content.clone());
2362                        head_content = Some(content);
2363                    }
2364                    Some(FileStatus::Untracked | FileStatus::Ignored) => {}
2365                    Some(FileStatus::Unmerged(unmerged_status)) => {
2366                        state
2367                            .unmerged_paths
2368                            .insert(repo_path.clone(), *unmerged_status);
2369                        content.push_str(" (unmerged)");
2370                        index_content = Some(content.clone());
2371                        head_content = Some(content);
2372                    }
2373                    Some(FileStatus::Tracked(TrackedStatus {
2374                        index_status,
2375                        worktree_status,
2376                    })) => {
2377                        match worktree_status {
2378                            StatusCode::Modified => {
2379                                let mut content = content.clone();
2380                                content.push_str(" (modified in working copy)");
2381                                index_content = Some(content);
2382                            }
2383                            StatusCode::TypeChanged | StatusCode::Unmodified => {
2384                                index_content = Some(content.clone());
2385                            }
2386                            StatusCode::Added => {}
2387                            StatusCode::Deleted | StatusCode::Renamed | StatusCode::Copied => {
2388                                panic!("cannot create these statuses for an existing file");
2389                            }
2390                        };
2391                        match index_status {
2392                            StatusCode::Modified => {
2393                                let mut content = index_content.clone().expect(
2394                                    "file cannot be both modified in index and created in working copy",
2395                                );
2396                                content.push_str(" (modified in index)");
2397                                head_content = Some(content);
2398                            }
2399                            StatusCode::TypeChanged | StatusCode::Unmodified => {
2400                                head_content = Some(index_content.clone().expect("file cannot be both unmodified in index and created in working copy"));
2401                            }
2402                            StatusCode::Added => {}
2403                            StatusCode::Deleted  => {
2404                                head_content = Some("".into());
2405                            }
2406                            StatusCode::Renamed | StatusCode::Copied => {
2407                                panic!("cannot create these statuses for an existing file");
2408                            }
2409                        };
2410                    }
2411                };
2412
2413                if let Some(content) = index_content {
2414                    state.index_contents.insert(repo_path.clone(), content);
2415                }
2416                if let Some(content) = head_content {
2417                    state.head_contents.insert(repo_path.clone(), content);
2418                }
2419            }
2420        }).unwrap();
2421    }
2422
2423    pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
2424        self.with_git_state(dot_git, true, |state| {
2425            state.simulated_index_write_error_message = message;
2426        })
2427        .unwrap();
2428    }
2429
2430    pub fn set_create_worktree_error(&self, dot_git: &Path, message: Option<String>) {
2431        self.with_git_state(dot_git, true, |state| {
2432            state.simulated_create_worktree_error = message;
2433        })
2434        .unwrap();
2435    }
2436
2437    /// Makes subsequent `remove_dir` calls for `path` fail with `message`.
2438    pub fn set_remove_dir_error(&self, path: impl AsRef<Path>, message: String) {
2439        self.state
2440            .lock()
2441            .remove_dir_errors
2442            .insert(Self::remove_dir_error_key(path.as_ref()), message);
2443    }
2444
2445    /// Entry resolution in `try_entry` ignores drive prefixes, so the error
2446    /// injection map must too.
2447    /// Otherwise, on Windows, a key like `C:\workspace\dir` would never match a
2448    /// lookup for `\workspace\dir`.
2449    fn remove_dir_error_key(path: &Path) -> PathBuf {
2450        normalize_path(path)
2451            .components()
2452            .skip_while(|component| matches!(component, Component::Prefix(_)))
2453            .collect()
2454    }
2455
2456    pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
2457        let mut result = Vec::new();
2458        let mut queue = collections::VecDeque::new();
2459        let state = &*self.state.lock();
2460        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
2461        while let Some((path, entry)) = queue.pop_front() {
2462            if let FakeFsEntry::Dir { entries, .. } = entry {
2463                for (name, entry) in entries {
2464                    queue.push_back((path.join(name), entry));
2465                }
2466            }
2467            if include_dot_git
2468                || !path
2469                    .components()
2470                    .any(|component| component.as_os_str() == *FS_DOT_GIT)
2471            {
2472                result.push(path);
2473            }
2474        }
2475        result
2476    }
2477
2478    pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
2479        let mut result = Vec::new();
2480        let mut queue = collections::VecDeque::new();
2481        let state = &*self.state.lock();
2482        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
2483        while let Some((path, entry)) = queue.pop_front() {
2484            if let FakeFsEntry::Dir { entries, .. } = entry {
2485                for (name, entry) in entries {
2486                    queue.push_back((path.join(name), entry));
2487                }
2488                if include_dot_git
2489                    || !path
2490                        .components()
2491                        .any(|component| component.as_os_str() == *FS_DOT_GIT)
2492                {
2493                    result.push(path);
2494                }
2495            }
2496        }
2497        result
2498    }
2499
2500    pub fn files(&self) -> Vec<PathBuf> {
2501        let mut result = Vec::new();
2502        let mut queue = collections::VecDeque::new();
2503        let state = &*self.state.lock();
2504        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
2505        while let Some((path, entry)) = queue.pop_front() {
2506            match entry {
2507                FakeFsEntry::File { .. } => result.push(path),
2508                FakeFsEntry::Dir { entries, .. } => {
2509                    for (name, entry) in entries {
2510                        queue.push_back((path.join(name), entry));
2511                    }
2512                }
2513                FakeFsEntry::Symlink { .. } => {}
2514            }
2515        }
2516        result
2517    }
2518
2519    pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec<u8>)> {
2520        let mut result = Vec::new();
2521        let mut queue = collections::VecDeque::new();
2522        let state = &*self.state.lock();
2523        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
2524        while let Some((path, entry)) = queue.pop_front() {
2525            match entry {
2526                FakeFsEntry::File { content, .. } => {
2527                    if path.starts_with(prefix) {
2528                        result.push((path, content.clone()));
2529                    }
2530                }
2531                FakeFsEntry::Dir { entries, .. } => {
2532                    for (name, entry) in entries {
2533                        queue.push_back((path.join(name), entry));
2534                    }
2535                }
2536                FakeFsEntry::Symlink { .. } => {}
2537            }
2538        }
2539        result
2540    }
2541
2542    /// How many `read_dir` calls have been issued.
2543    pub fn read_dir_call_count(&self) -> usize {
2544        self.state.lock().read_dir_call_count
2545    }
2546
2547    pub fn watched_paths(&self) -> Vec<PathBuf> {
2548        let state = self.state.lock();
2549        state
2550            .event_txs
2551            .iter()
2552            .filter_map(|(path, tx)| Some(path.clone()).filter(|_| !tx.is_closed()))
2553            .collect()
2554    }
2555
2556    /// How many `metadata` calls have been issued.
2557    pub fn metadata_call_count(&self) -> usize {
2558        self.state.lock().metadata_call_count
2559    }
2560
2561    /// How many write operations have been issued for a specific path.
2562    pub fn write_count_for_path(&self, path: impl AsRef<Path>) -> usize {
2563        let path = path.as_ref().to_path_buf();
2564        self.state
2565            .lock()
2566            .path_write_counts
2567            .get(&path)
2568            .copied()
2569            .unwrap_or(0)
2570    }
2571
2572    pub fn emit_fs_event(&self, path: impl Into<PathBuf>, event: Option<PathEventKind>) {
2573        self.state.lock().emit_event(std::iter::once((path, event)));
2574    }
2575
2576    fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
2577        self.executor.simulate_random_delay()
2578    }
2579
2580    async fn remove_dir_inner(
2581        &self,
2582        path: &Path,
2583        options: RemoveOptions,
2584    ) -> Result<Option<FakeFsEntry>> {
2585        self.simulate_random_delay().await;
2586
2587        let path = normalize_path(path);
2588        if let Some(message) = self
2589            .state
2590            .lock()
2591            .remove_dir_errors
2592            .get(&Self::remove_dir_error_key(&path))
2593        {
2594            anyhow::bail!("{message}");
2595        }
2596        let parent_path = path.parent().context("cannot remove the root")?;
2597        let base_name = path.file_name().context("cannot remove the root")?;
2598
2599        let mut state = self.state.lock();
2600        let parent_entry = state.entry(parent_path)?;
2601        let entry = parent_entry
2602            .dir_entries(parent_path)?
2603            .entry(base_name.to_str().unwrap().into());
2604
2605        let removed = match entry {
2606            btree_map::Entry::Vacant(_) => {
2607                if !options.ignore_if_not_exists {
2608                    anyhow::bail!("{path:?} does not exist");
2609                }
2610
2611                None
2612            }
2613            btree_map::Entry::Occupied(mut entry) => {
2614                {
2615                    let children = entry.get_mut().dir_entries(&path)?;
2616                    if !options.recursive && !children.is_empty() {
2617                        anyhow::bail!("{path:?} is not empty");
2618                    }
2619                }
2620
2621                Some(entry.remove())
2622            }
2623        };
2624
2625        state.emit_event([(path, Some(PathEventKind::Removed))]);
2626        Ok(removed)
2627    }
2628
2629    async fn remove_file_inner(
2630        &self,
2631        path: &Path,
2632        options: RemoveOptions,
2633    ) -> Result<Option<FakeFsEntry>> {
2634        self.simulate_random_delay().await;
2635
2636        let path = normalize_path(path);
2637        let parent_path = path.parent().context("cannot remove the root")?;
2638        let base_name = path.file_name().unwrap();
2639        let mut state = self.state.lock();
2640        let parent_entry = state.entry(parent_path)?;
2641        let entry = parent_entry
2642            .dir_entries(parent_path)?
2643            .entry(base_name.to_str().unwrap().into());
2644        let removed = match entry {
2645            btree_map::Entry::Vacant(_) => {
2646                if !options.ignore_if_not_exists {
2647                    anyhow::bail!("{path:?} does not exist");
2648                }
2649
2650                None
2651            }
2652            btree_map::Entry::Occupied(mut entry) => {
2653                entry.get_mut().file_content(&path)?;
2654                Some(entry.remove())
2655            }
2656        };
2657
2658        state.emit_event([(path, Some(PathEventKind::Removed))]);
2659        Ok(removed)
2660    }
2661
2662    pub fn trashed_paths(&self) -> Vec<PathBuf> {
2663        self.state
2664            .lock()
2665            .trash
2666            .lock()
2667            .values()
2668            .map(|(trashed_entry, _fake_entry)| {
2669                PathBuf::new()
2670                    .join(trashed_entry.original_parent.clone())
2671                    .join(trashed_entry.name.clone())
2672            })
2673            .collect::<Vec<PathBuf>>()
2674    }
2675}
2676
2677#[cfg(feature = "test-support")]
2678impl FakeFsEntry {
2679    fn is_file(&self) -> bool {
2680        matches!(self, Self::File { .. })
2681    }
2682
2683    fn is_symlink(&self) -> bool {
2684        matches!(self, Self::Symlink { .. })
2685    }
2686
2687    fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
2688        if let Self::File { content, .. } = self {
2689            Ok(content)
2690        } else {
2691            anyhow::bail!("not a file: {path:?}");
2692        }
2693    }
2694
2695    fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap<String, FakeFsEntry>> {
2696        if let Self::Dir { entries, .. } = self {
2697            Ok(entries)
2698        } else {
2699            anyhow::bail!("not a directory: {path:?}");
2700        }
2701    }
2702}
2703
2704#[cfg(feature = "test-support")]
2705struct FakeWatcher {
2706    tx: async_channel::Sender<Vec<PathEvent>>,
2707    fs_state: Arc<Mutex<FakeFsState>>,
2708    prefixes: Mutex<Vec<PathBuf>>,
2709}
2710
2711#[cfg(feature = "test-support")]
2712impl Watcher for FakeWatcher {
2713    fn add(&self, path: &Path) -> Result<()> {
2714        let path = normalize_path(path);
2715        self.fs_state
2716            .try_lock()
2717            .unwrap()
2718            .create_file_before_watch_add(&path)?;
2719
2720        let mut prefixes = self.prefixes.lock();
2721        if prefixes.iter().any(|prefix| path.starts_with(prefix)) {
2722            return Ok(());
2723        }
2724
2725        self.fs_state
2726            .try_lock()
2727            .unwrap()
2728            .event_txs
2729            .push((path.clone(), self.tx.clone()));
2730        prefixes.push(path);
2731        Ok(())
2732    }
2733
2734    fn remove(&self, path: &Path) -> Result<()> {
2735        let path = normalize_path(path);
2736        self.prefixes.lock().retain(|prefix| prefix != &path);
2737        self.fs_state
2738            .try_lock()
2739            .unwrap()
2740            .event_txs
2741            .retain(|(watched_path, _)| watched_path != &path);
2742        Ok(())
2743    }
2744}
2745
2746#[cfg(feature = "test-support")]
2747#[derive(Debug)]
2748struct FakeHandle {
2749    inode: u64,
2750}
2751
2752#[cfg(feature = "test-support")]
2753impl FileHandle for FakeHandle {
2754    fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
2755        let fs = fs.as_fake();
2756        let mut state = fs.state.lock();
2757        let Some(target) = state.moves.get(&self.inode).cloned() else {
2758            anyhow::bail!("fake fd not moved")
2759        };
2760
2761        if state.try_entry(&target, false).is_some() {
2762            return Ok(target);
2763        }
2764        anyhow::bail!("fake fd target not found")
2765    }
2766}
2767
2768#[cfg(feature = "test-support")]
2769#[async_trait::async_trait]
2770impl Fs for FakeFs {
2771    async fn create_dir(&self, path: &Path) -> Result<()> {
2772        self.simulate_random_delay().await;
2773
2774        let mut created_dirs = Vec::new();
2775        let mut cur_path = PathBuf::new();
2776        for component in path.components() {
2777            let should_skip = matches!(component, Component::Prefix(..) | Component::RootDir);
2778            cur_path.push(component);
2779            if should_skip {
2780                continue;
2781            }
2782            let mut state = self.state.lock();
2783
2784            let inode = state.get_and_increment_inode();
2785            let mtime = state.get_and_increment_mtime();
2786            state.write_path(&cur_path, |entry| {
2787                entry.or_insert_with(|| {
2788                    created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
2789                    FakeFsEntry::Dir {
2790                        inode,
2791                        mtime,
2792                        len: 0,
2793                        entries: Default::default(),
2794                        git_repo_state: None,
2795                    }
2796                });
2797                Ok(())
2798            })?
2799        }
2800
2801        self.state.lock().emit_event(created_dirs);
2802        Ok(())
2803    }
2804
2805    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
2806        self.simulate_random_delay().await;
2807        let mut state = self.state.lock();
2808        let inode = state.get_and_increment_inode();
2809        let mtime = state.get_and_increment_mtime();
2810        let file = FakeFsEntry::File {
2811            inode,
2812            mtime,
2813            len: 0,
2814            content: Vec::new(),
2815            git_dir_path: None,
2816        };
2817        let mut kind = Some(PathEventKind::Created);
2818        state.write_path(path, |entry| {
2819            match entry {
2820                btree_map::Entry::Occupied(mut e) => {
2821                    if options.overwrite {
2822                        kind = Some(PathEventKind::Changed);
2823                        *e.get_mut() = file;
2824                    } else if !options.ignore_if_exists {
2825                        anyhow::bail!("path already exists: {path:?}");
2826                    }
2827                }
2828                btree_map::Entry::Vacant(e) => {
2829                    e.insert(file);
2830                }
2831            }
2832            Ok(())
2833        })?;
2834        state.emit_event([(path, kind)]);
2835        Ok(())
2836    }
2837
2838    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
2839        let mut state = self.state.lock();
2840        let file = FakeFsEntry::Symlink { target };
2841        state
2842            .write_path(path.as_ref(), move |e| match e {
2843                btree_map::Entry::Vacant(e) => {
2844                    e.insert(file);
2845                    Ok(())
2846                }
2847                btree_map::Entry::Occupied(mut e) => {
2848                    *e.get_mut() = file;
2849                    Ok(())
2850                }
2851            })
2852            .unwrap();
2853        state.emit_event([(path, Some(PathEventKind::Created))]);
2854
2855        Ok(())
2856    }
2857
2858    async fn create_file_with(
2859        &self,
2860        path: &Path,
2861        mut content: Pin<&mut (dyn AsyncRead + Send)>,
2862    ) -> Result<()> {
2863        let mut bytes = Vec::new();
2864        content.read_to_end(&mut bytes).await?;
2865        self.write_file_internal(path, bytes, true)?;
2866        Ok(())
2867    }
2868
2869    async fn extract_tar_file(
2870        &self,
2871        path: &Path,
2872        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
2873    ) -> Result<()> {
2874        let mut entries = content.entries()?;
2875        while let Some(entry) = entries.next().await {
2876            let mut entry = entry?;
2877            if entry.header().entry_type().is_file() {
2878                let path = path.join(entry.path()?.as_ref());
2879                let mut bytes = Vec::new();
2880                entry.read_to_end(&mut bytes).await?;
2881                self.create_dir(path.parent().unwrap()).await?;
2882                self.write_file_internal(&path, bytes, true)?;
2883            }
2884        }
2885        Ok(())
2886    }
2887
2888    async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
2889        self.simulate_random_delay().await;
2890
2891        let old_path = normalize_path(old_path);
2892        let new_path = normalize_path(new_path);
2893
2894        if options.create_parents {
2895            if let Some(parent) = new_path.parent() {
2896                self.create_dir(parent).await?;
2897            }
2898        }
2899
2900        let mut state = self.state.lock();
2901        let moved_entry = state.write_path(&old_path, |e| {
2902            if let btree_map::Entry::Occupied(e) = e {
2903                Ok(e.get().clone())
2904            } else {
2905                anyhow::bail!("path does not exist: {old_path:?}")
2906            }
2907        })?;
2908
2909        let inode = match moved_entry {
2910            FakeFsEntry::File { inode, .. } => inode,
2911            FakeFsEntry::Dir { inode, .. } => inode,
2912            _ => 0,
2913        };
2914
2915        state.moves.insert(inode, new_path.clone());
2916
2917        state.write_path(&new_path, |e| {
2918            match e {
2919                btree_map::Entry::Occupied(mut e) => {
2920                    if options.overwrite {
2921                        *e.get_mut() = moved_entry;
2922                    } else if !options.ignore_if_exists {
2923                        anyhow::bail!("path already exists: {new_path:?}");
2924                    }
2925                }
2926                btree_map::Entry::Vacant(e) => {
2927                    e.insert(moved_entry);
2928                }
2929            }
2930            Ok(())
2931        })?;
2932
2933        state
2934            .write_path(&old_path, |e| {
2935                if let btree_map::Entry::Occupied(e) = e {
2936                    Ok(e.remove())
2937                } else {
2938                    unreachable!()
2939                }
2940            })
2941            .unwrap();
2942
2943        state.emit_event([
2944            (old_path, Some(PathEventKind::Removed)),
2945            (new_path, Some(PathEventKind::Created)),
2946        ]);
2947        Ok(())
2948    }
2949
2950    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
2951        self.simulate_random_delay().await;
2952
2953        let source = normalize_path(source);
2954        let target = normalize_path(target);
2955        let mut state = self.state.lock();
2956        let mtime = state.get_and_increment_mtime();
2957        let inode = state.get_and_increment_inode();
2958        let source_entry = state.entry(&source)?;
2959        let content = source_entry.file_content(&source)?.clone();
2960        let mut kind = Some(PathEventKind::Created);
2961        state.write_path(&target, |e| match e {
2962            btree_map::Entry::Occupied(e) => {
2963                if options.overwrite {
2964                    kind = Some(PathEventKind::Changed);
2965                    Ok(Some(e.get().clone()))
2966                } else if !options.ignore_if_exists {
2967                    anyhow::bail!("{target:?} already exists");
2968                } else {
2969                    Ok(None)
2970                }
2971            }
2972            btree_map::Entry::Vacant(e) => Ok(Some(
2973                e.insert(FakeFsEntry::File {
2974                    inode,
2975                    mtime,
2976                    len: content.len() as u64,
2977                    content,
2978                    git_dir_path: None,
2979                })
2980                .clone(),
2981            )),
2982        })?;
2983        state.emit_event([(target, kind)]);
2984        Ok(())
2985    }
2986
2987    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
2988        self.remove_dir_inner(path, options).await.map(|_| ())
2989    }
2990
2991    async fn trash(&self, path: &Path, options: RemoveOptions) -> Result<TrashId> {
2992        let normalized_path = normalize_path(path);
2993        let parent_path = normalized_path.parent().context("cannot remove the root")?;
2994        let base_name = normalized_path.file_name().unwrap();
2995        let result = if self.is_dir(path).await {
2996            self.remove_dir_inner(path, options).await?
2997        } else {
2998            self.remove_file_inner(path, options).await?
2999        };
3000
3001        match result {
3002            Some(fake_entry) => {
3003                let trashed_entry = TrashedEntry {
3004                    id: base_name.to_str().unwrap().into(),
3005                    name: base_name.to_str().unwrap().into(),
3006                    original_parent: parent_path.to_path_buf(),
3007                };
3008
3009                let trash_id = self
3010                    .state
3011                    .lock()
3012                    .trash
3013                    .lock()
3014                    .insert((trashed_entry, fake_entry));
3015
3016                Ok(trash_id)
3017            }
3018            None => anyhow::bail!("{normalized_path:?} does not exist"),
3019        }
3020    }
3021
3022    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
3023        self.remove_file_inner(path, options).await.map(|_| ())
3024    }
3025
3026    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>> {
3027        let bytes = self.load_internal(path).await?;
3028        Ok(Box::new(io::Cursor::new(bytes)))
3029    }
3030
3031    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
3032        self.simulate_random_delay().await;
3033        let mut state = self.state.lock();
3034        let inode = match state.entry(path)? {
3035            FakeFsEntry::File { inode, .. } => *inode,
3036            FakeFsEntry::Dir { inode, .. } => *inode,
3037            _ => unreachable!(),
3038        };
3039        Ok(Arc::new(FakeHandle { inode }))
3040    }
3041
3042    async fn load(&self, path: &Path) -> Result<String> {
3043        let content = self.load_internal(path).await?;
3044        Ok(String::from_utf8(content)?)
3045    }
3046
3047    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
3048        self.load_internal(path).await
3049    }
3050
3051    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
3052        self.simulate_random_delay().await;
3053        let path = normalize_path(path.as_path());
3054        if let Some(path) = path.parent() {
3055            self.create_dir(path).await?;
3056        }
3057        self.write_file_internal(path, data.into_bytes(), true)?;
3058        Ok(())
3059    }
3060
3061    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
3062        self.simulate_random_delay().await;
3063        let path = normalize_path(path);
3064        let content = text::chunks_with_line_ending(text, line_ending).collect::<String>();
3065        if let Some(path) = path.parent() {
3066            self.create_dir(path).await?;
3067        }
3068        self.write_file_internal(path, content.into_bytes(), false)?;
3069        Ok(())
3070    }
3071
3072    async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
3073        self.simulate_random_delay().await;
3074        let path = normalize_path(path);
3075        if let Some(path) = path.parent() {
3076            self.create_dir(path).await?;
3077        }
3078        self.write_file_internal(path, content.to_vec(), false)?;
3079        Ok(())
3080    }
3081
3082    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
3083        let path = normalize_path(path);
3084        self.simulate_random_delay().await;
3085        let state = self.state.lock();
3086        let canonical_path = state
3087            .canonicalize(&path, true)
3088            .with_context(|| format!("path does not exist: {path:?}"))?;
3089        Ok(canonical_path)
3090    }
3091
3092    async fn is_file(&self, path: &Path) -> bool {
3093        let path = normalize_path(path);
3094        self.simulate_random_delay().await;
3095        let mut state = self.state.lock();
3096        if let Some((entry, _)) = state.try_entry(&path, true) {
3097            entry.is_file()
3098        } else {
3099            false
3100        }
3101    }
3102
3103    async fn is_dir(&self, path: &Path) -> bool {
3104        self.metadata(path)
3105            .await
3106            .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
3107    }
3108
3109    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
3110        self.simulate_random_delay().await;
3111        let path = normalize_path(path);
3112        let mut state = self.state.lock();
3113        state.metadata_call_count += 1;
3114        if let Some((mut entry, _)) = state.try_entry(&path, false) {
3115            let is_symlink = entry.is_symlink();
3116            if is_symlink {
3117                if let Some(e) = state.try_entry(&path, true).map(|e| e.0) {
3118                    entry = e;
3119                } else {
3120                    return Ok(None);
3121                }
3122            }
3123
3124            Ok(Some(match &*entry {
3125                FakeFsEntry::File {
3126                    inode, mtime, len, ..
3127                } => Metadata {
3128                    inode: *inode,
3129                    mtime: *mtime,
3130                    len: *len,
3131                    is_dir: false,
3132                    is_symlink,
3133                    is_fifo: false,
3134                    is_executable: false,
3135                    is_writable: true,
3136                },
3137                FakeFsEntry::Dir {
3138                    inode, mtime, len, ..
3139                } => Metadata {
3140                    inode: *inode,
3141                    mtime: *mtime,
3142                    len: *len,
3143                    is_dir: true,
3144                    is_symlink,
3145                    is_fifo: false,
3146                    is_executable: false,
3147                    is_writable: true,
3148                },
3149                FakeFsEntry::Symlink { .. } => unreachable!(),
3150            }))
3151        } else {
3152            Ok(None)
3153        }
3154    }
3155
3156    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
3157        self.simulate_random_delay().await;
3158        let path = normalize_path(path);
3159        let mut state = self.state.lock();
3160        let (entry, _) = state
3161            .try_entry(&path, false)
3162            .with_context(|| format!("path does not exist: {path:?}"))?;
3163        if let FakeFsEntry::Symlink { target } = entry {
3164            Ok(target.clone())
3165        } else {
3166            anyhow::bail!("not a symlink: {path:?}")
3167        }
3168    }
3169
3170    async fn read_dir(
3171        &self,
3172        path: &Path,
3173    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
3174        self.simulate_random_delay().await;
3175        let path = normalize_path(path);
3176        let mut state = self.state.lock();
3177        state.read_dir_call_count += 1;
3178        let entry = state.entry(&path)?;
3179        let children = entry.dir_entries(&path)?;
3180        let paths = children
3181            .keys()
3182            .map(|file_name| Ok(path.join(file_name)))
3183            .collect::<Vec<_>>();
3184        Ok(Box::pin(futures::stream::iter(paths)))
3185    }
3186
3187    async fn watch(
3188        &self,
3189        path: &Path,
3190        _: Duration,
3191    ) -> (
3192        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
3193        Arc<dyn Watcher>,
3194    ) {
3195        self.simulate_random_delay().await;
3196        let (tx, rx) = async_channel::unbounded();
3197        let path = path.to_path_buf();
3198        self.state.lock().event_txs.push((path.clone(), tx.clone()));
3199        let executor = self.executor.clone();
3200        let watcher = Arc::new(FakeWatcher {
3201            tx,
3202            fs_state: self.state.clone(),
3203            prefixes: Mutex::new(vec![path]),
3204        });
3205        (
3206            Box::pin(futures::StreamExt::filter(rx, {
3207                let watcher = watcher.clone();
3208                move |events| {
3209                    let result = events.iter().any(|evt_path| {
3210                        watcher
3211                            .prefixes
3212                            .lock()
3213                            .iter()
3214                            .any(|prefix| evt_path.path.starts_with(prefix))
3215                    });
3216                    let executor = executor.clone();
3217                    async move {
3218                        executor.simulate_random_delay().await;
3219                        result
3220                    }
3221                }
3222            })),
3223            watcher,
3224        )
3225    }
3226
3227    fn open_repo(
3228        &self,
3229        abs_dot_git: &Path,
3230        _system_git_binary: Option<&Path>,
3231    ) -> Result<Arc<dyn GitRepository>> {
3232        self.with_git_state_and_paths(
3233            abs_dot_git,
3234            false,
3235            |_, repository_dir_path, common_dir_path| {
3236                Arc::new(fake_git_repo::FakeGitRepository {
3237                    fs: self.this.upgrade().unwrap(),
3238                    executor: self.executor.clone(),
3239                    dot_git_path: abs_dot_git.to_path_buf(),
3240                    repository_dir_path: repository_dir_path.to_owned(),
3241                    common_dir_path: common_dir_path.to_owned(),
3242                    checkpoints: Arc::default(),
3243                    is_trusted: Arc::default(),
3244                }) as _
3245            },
3246        )
3247    }
3248
3249    async fn git_init(
3250        &self,
3251        abs_work_directory_path: &Path,
3252        _fallback_branch_name: String,
3253    ) -> Result<()> {
3254        self.create_dir(&abs_work_directory_path.join(".git")).await
3255    }
3256
3257    async fn git_clone(&self, _abs_work_directory: &Path, _repo_url: &str) -> Result<()> {
3258        anyhow::bail!("Git clone is not supported in fake Fs")
3259    }
3260
3261    async fn git_config(&self, _abs_work_directory: &Path, _args: Vec<String>) -> Result<String> {
3262        anyhow::bail!("Git config is not supported in fake Fs")
3263    }
3264
3265    fn is_fake(&self) -> bool {
3266        true
3267    }
3268
3269    async fn is_case_sensitive(&self) -> bool {
3270        true
3271    }
3272
3273    fn subscribe_to_jobs(&self) -> JobEventReceiver {
3274        let (sender, receiver) = futures::channel::mpsc::unbounded();
3275        self.state.lock().job_event_subscribers.lock().push(sender);
3276        receiver
3277    }
3278
3279    async fn restore(&self, trash_id: TrashId) -> Result<PathBuf, TrashRestoreError> {
3280        let mut state = self.state.lock();
3281
3282        let Some((trashed_entry, fake_entry)) = state.trash.lock().remove(trash_id) else {
3283            return Err(TrashRestoreError::AlreadyRestored);
3284        };
3285
3286        let path = trashed_entry
3287            .original_parent
3288            .join(trashed_entry.name.clone());
3289
3290        let result = state.write_path(&path, |entry| match entry {
3291            btree_map::Entry::Vacant(entry) => {
3292                entry.insert(fake_entry);
3293                Ok(())
3294            }
3295            btree_map::Entry::Occupied(_) => {
3296                anyhow::bail!("Failed to restore {:?}", path);
3297            }
3298        });
3299
3300        match result {
3301            Ok(_) => {
3302                state.emit_event([(path.clone(), Some(PathEventKind::Created))]);
3303                Ok(path)
3304            }
3305            Err(_) => {
3306                // For now we'll just assume that this failed because it was a
3307                // collision error, which I think that, for the time being, is
3308                // the only case where this could fail?
3309                Err(TrashRestoreError::Collision { path })
3310            }
3311        }
3312    }
3313
3314    #[cfg(feature = "test-support")]
3315    fn as_fake(&self) -> Arc<FakeFs> {
3316        self.this.upgrade().unwrap()
3317    }
3318}
3319
3320pub async fn copy_recursive<'a>(
3321    fs: &'a dyn Fs,
3322    source: &'a Path,
3323    target: &'a Path,
3324    options: CopyOptions,
3325) -> Result<()> {
3326    for (item, is_dir) in read_dir_items(fs, source).await? {
3327        let Ok(item_relative_path) = item.strip_prefix(source) else {
3328            continue;
3329        };
3330        let target_item = if item_relative_path == Path::new("") {
3331            target.to_path_buf()
3332        } else {
3333            target.join(item_relative_path)
3334        };
3335        if is_dir {
3336            if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
3337                if options.ignore_if_exists {
3338                    continue;
3339                } else {
3340                    anyhow::bail!("{target_item:?} already exists");
3341                }
3342            }
3343            let _ = fs
3344                .remove_dir(
3345                    &target_item,
3346                    RemoveOptions {
3347                        recursive: true,
3348                        ignore_if_not_exists: true,
3349                    },
3350                )
3351                .await;
3352            fs.create_dir(&target_item).await?;
3353        } else {
3354            fs.copy_file(&item, &target_item, options).await?;
3355        }
3356    }
3357    Ok(())
3358}
3359
3360/// Recursively reads all of the paths in the given directory.
3361///
3362/// Returns a vector of tuples of (path, is_dir).
3363pub async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(PathBuf, bool)>> {
3364    let mut items = Vec::new();
3365    read_recursive(fs, source, &mut items).await?;
3366    Ok(items)
3367}
3368
3369fn read_recursive<'a>(
3370    fs: &'a dyn Fs,
3371    source: &'a Path,
3372    output: &'a mut Vec<(PathBuf, bool)>,
3373) -> BoxFuture<'a, Result<()>> {
3374    use futures::future::FutureExt;
3375
3376    async move {
3377        let metadata = fs
3378            .metadata(source)
3379            .await?
3380            .with_context(|| format!("path does not exist: {source:?}"))?;
3381
3382        if metadata.is_dir {
3383            output.push((source.to_path_buf(), true));
3384            let mut children = fs.read_dir(source).await?;
3385            while let Some(child_path) = children.next().await {
3386                if let Ok(child_path) = child_path {
3387                    read_recursive(fs, &child_path, output).await?;
3388                }
3389            }
3390        } else {
3391            output.push((source.to_path_buf(), false));
3392        }
3393        Ok(())
3394    }
3395    .boxed()
3396}
3397
3398// todo(windows)
3399// can we get file id not open the file twice?
3400// https://github.com/rust-lang/rust/issues/63010
3401#[cfg(target_os = "windows")]
3402async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
3403    use std::os::windows::io::AsRawHandle;
3404
3405    use smol::fs::windows::OpenOptionsExt;
3406    use windows::Win32::{
3407        Foundation::HANDLE,
3408        Storage::FileSystem::{
3409            BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS, GetFileInformationByHandle,
3410        },
3411    };
3412
3413    let file = smol::fs::OpenOptions::new()
3414        .read(true)
3415        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
3416        .open(path)
3417        .await?;
3418
3419    let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
3420    // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
3421    // This function supports Windows XP+
3422    smol::unblock(move || {
3423        unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
3424
3425        Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
3426    })
3427    .await
3428}
3429
3430#[cfg(target_os = "windows")]
3431fn atomic_replace<P: AsRef<Path>>(
3432    replaced_file: P,
3433    replacement_file: P,
3434) -> windows::core::Result<()> {
3435    use windows::{
3436        Win32::Storage::FileSystem::{REPLACE_FILE_FLAGS, ReplaceFileW},
3437        core::HSTRING,
3438    };
3439
3440    // If the file does not exist, create it.
3441    let _ = std::fs::File::create_new(replaced_file.as_ref());
3442
3443    unsafe {
3444        ReplaceFileW(
3445            &HSTRING::from(replaced_file.as_ref().to_string_lossy().into_owned()),
3446            &HSTRING::from(replacement_file.as_ref().to_string_lossy().into_owned()),
3447            None,
3448            REPLACE_FILE_FLAGS::default(),
3449            None,
3450            None,
3451        )
3452    }
3453}
3454
Served at tenant.openagents/omega Member data and write actions are omitted.