fs.rs

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