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