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