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_rs::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
698 // BOM for UTF-16 is written at the start of the file here because
699 // if BOM is written in the `encode` function of `fs::encodings`, it would be written
700 // for every chunk, resulting in multiple BOMs in the file.
701 if encoding.get_encoding() == encoding_rs::UTF_16BE {
702 // Write BOM for UTF-16BE
703 writer.write_all(&[0xFE, 0xFF]).await?;
704 } else if encoding.get_encoding() == encoding_rs::UTF_16LE {
705 // Write BOM for UTF-16LE
706 writer.write_all(&[0xFF, 0xFE]).await?;
707 }
708
709 for chunk in chunks(text, line_ending) {
710 writer
711 .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?)
712 .await?
713 }
714
715 writer.flush().await?;
716 Ok(())
717 }
718
719 async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
720 if let Some(path) = path.parent() {
721 self.create_dir(path).await?;
722 }
723 let path = path.to_owned();
724 let contents = content.to_owned();
725 self.executor
726 .spawn(async move {
727 std::fs::write(path, contents)?;
728 Ok(())
729 })
730 .await
731 }
732
733 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
734 let path = path.to_owned();
735 self.executor
736 .spawn(async move {
737 std::fs::canonicalize(&path).with_context(|| format!("canonicalizing {path:?}"))
738 })
739 .await
740 }
741
742 async fn is_file(&self, path: &Path) -> bool {
743 let path = path.to_owned();
744 self.executor
745 .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) })
746 .await
747 }
748
749 async fn is_dir(&self, path: &Path) -> bool {
750 let path = path.to_owned();
751 self.executor
752 .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) })
753 .await
754 }
755
756 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
757 let path_buf = path.to_owned();
758 let symlink_metadata = match self
759 .executor
760 .spawn(async move { std::fs::symlink_metadata(&path_buf) })
761 .await
762 {
763 Ok(metadata) => metadata,
764 Err(err) => {
765 return match (err.kind(), err.raw_os_error()) {
766 (io::ErrorKind::NotFound, _) => Ok(None),
767 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
768 _ => Err(anyhow::Error::new(err)),
769 };
770 }
771 };
772
773 let is_symlink = symlink_metadata.file_type().is_symlink();
774 let metadata = if is_symlink {
775 let path_buf = path.to_path_buf();
776 let path_exists = self
777 .executor
778 .spawn(async move {
779 path_buf
780 .try_exists()
781 .with_context(|| format!("checking existence for path {path_buf:?}"))
782 })
783 .await?;
784 if path_exists {
785 let path_buf = path.to_path_buf();
786 self.executor
787 .spawn(async move { std::fs::metadata(path_buf) })
788 .await
789 .with_context(|| "accessing symlink for path {path}")?
790 } else {
791 symlink_metadata
792 }
793 } else {
794 symlink_metadata
795 };
796
797 #[cfg(unix)]
798 let inode = metadata.ino();
799
800 #[cfg(windows)]
801 let inode = file_id(path).await?;
802
803 #[cfg(windows)]
804 let is_fifo = false;
805
806 #[cfg(unix)]
807 let is_fifo = metadata.file_type().is_fifo();
808
809 Ok(Some(Metadata {
810 inode,
811 mtime: MTime(metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH)),
812 len: metadata.len(),
813 is_symlink,
814 is_dir: metadata.file_type().is_dir(),
815 is_fifo,
816 }))
817 }
818
819 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
820 let path = path.to_owned();
821 let path = self
822 .executor
823 .spawn(async move { std::fs::read_link(&path) })
824 .await?;
825 Ok(path)
826 }
827
828 async fn read_dir(
829 &self,
830 path: &Path,
831 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
832 let path = path.to_owned();
833 let result = iter(
834 self.executor
835 .spawn(async move { std::fs::read_dir(path) })
836 .await?,
837 )
838 .map(|entry| match entry {
839 Ok(entry) => Ok(entry.path()),
840 Err(error) => Err(anyhow!("failed to read dir entry {error:?}")),
841 });
842 Ok(Box::pin(result))
843 }
844
845 #[cfg(target_os = "macos")]
846 async fn watch(
847 &self,
848 path: &Path,
849 latency: Duration,
850 ) -> (
851 Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
852 Arc<dyn Watcher>,
853 ) {
854 use fsevent::StreamFlags;
855
856 let (events_tx, events_rx) = smol::channel::unbounded();
857 let handles = Arc::new(parking_lot::Mutex::new(collections::BTreeMap::default()));
858 let watcher = Arc::new(mac_watcher::MacWatcher::new(
859 events_tx,
860 Arc::downgrade(&handles),
861 latency,
862 ));
863 watcher.add(path).expect("handles can't be dropped");
864
865 (
866 Box::pin(
867 events_rx
868 .map(|events| {
869 events
870 .into_iter()
871 .map(|event| {
872 log::trace!("fs path event: {event:?}");
873 let kind = if event.flags.contains(StreamFlags::ITEM_REMOVED) {
874 Some(PathEventKind::Removed)
875 } else if event.flags.contains(StreamFlags::ITEM_CREATED) {
876 Some(PathEventKind::Created)
877 } else if event.flags.contains(StreamFlags::ITEM_MODIFIED)
878 | event.flags.contains(StreamFlags::ITEM_RENAMED)
879 {
880 Some(PathEventKind::Changed)
881 } else {
882 None
883 };
884 PathEvent {
885 path: event.path,
886 kind,
887 }
888 })
889 .collect()
890 })
891 .chain(futures::stream::once(async move {
892 drop(handles);
893 vec![]
894 })),
895 ),
896 watcher,
897 )
898 }
899
900 #[cfg(not(target_os = "macos"))]
901 async fn watch(
902 &self,
903 path: &Path,
904 latency: Duration,
905 ) -> (
906 Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
907 Arc<dyn Watcher>,
908 ) {
909 use parking_lot::Mutex;
910 use util::{ResultExt as _, paths::SanitizedPath};
911
912 let (tx, rx) = smol::channel::unbounded();
913 let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
914 let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
915
916 // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
917 if let Err(e) = watcher.add(path)
918 && let Some(parent) = path.parent()
919 && let Err(parent_e) = watcher.add(parent)
920 {
921 log::warn!(
922 "Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}",
923 path.display(),
924 parent.display()
925 );
926 }
927
928 // Check if path is a symlink and follow the target parent
929 if let Some(mut target) = self.read_link(path).await.ok() {
930 log::trace!("watch symlink {path:?} -> {target:?}");
931 // Check if symlink target is relative path, if so make it absolute
932 if target.is_relative()
933 && let Some(parent) = path.parent()
934 {
935 target = parent.join(target);
936 if let Ok(canonical) = self.canonicalize(&target).await {
937 target = SanitizedPath::new(&canonical).as_path().to_path_buf();
938 }
939 }
940 watcher.add(&target).ok();
941 if let Some(parent) = target.parent() {
942 watcher.add(parent).log_err();
943 }
944 }
945
946 (
947 Box::pin(rx.filter_map({
948 let watcher = watcher.clone();
949 move |_| {
950 let _ = watcher.clone();
951 let pending_paths = pending_paths.clone();
952 async move {
953 smol::Timer::after(latency).await;
954 let paths = std::mem::take(&mut *pending_paths.lock());
955 (!paths.is_empty()).then_some(paths)
956 }
957 }
958 })),
959 watcher,
960 )
961 }
962
963 fn open_repo(
964 &self,
965 dotgit_path: &Path,
966 system_git_binary_path: Option<&Path>,
967 ) -> Option<Arc<dyn GitRepository>> {
968 Some(Arc::new(RealGitRepository::new(
969 dotgit_path,
970 self.bundled_git_binary_path.clone(),
971 system_git_binary_path.map(|path| path.to_path_buf()),
972 self.executor.clone(),
973 )?))
974 }
975
976 async fn git_init(
977 &self,
978 abs_work_directory_path: &Path,
979 fallback_branch_name: String,
980 ) -> Result<()> {
981 let config = new_smol_command("git")
982 .current_dir(abs_work_directory_path)
983 .args(&["config", "--global", "--get", "init.defaultBranch"])
984 .output()
985 .await?;
986
987 let branch_name;
988
989 if config.status.success() && !config.stdout.is_empty() {
990 branch_name = String::from_utf8_lossy(&config.stdout);
991 } else {
992 branch_name = Cow::Borrowed(fallback_branch_name.as_str());
993 }
994
995 new_smol_command("git")
996 .current_dir(abs_work_directory_path)
997 .args(&["init", "-b"])
998 .arg(branch_name.trim())
999 .output()
1000 .await?;
1001
1002 Ok(())
1003 }
1004
1005 async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> {
1006 let output = new_smol_command("git")
1007 .current_dir(abs_work_directory)
1008 .args(&["clone", repo_url])
1009 .output()
1010 .await?;
1011
1012 if !output.status.success() {
1013 anyhow::bail!(
1014 "git clone failed: {}",
1015 String::from_utf8_lossy(&output.stderr)
1016 );
1017 }
1018
1019 Ok(())
1020 }
1021
1022 fn is_fake(&self) -> bool {
1023 false
1024 }
1025
1026 /// Checks whether the file system is case sensitive by attempting to create two files
1027 /// that have the same name except for the casing.
1028 ///
1029 /// It creates both files in a temporary directory it removes at the end.
1030 async fn is_case_sensitive(&self) -> Result<bool> {
1031 let temp_dir = TempDir::new()?;
1032 let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
1033 let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
1034
1035 let create_opts = CreateOptions {
1036 overwrite: false,
1037 ignore_if_exists: false,
1038 };
1039
1040 // Create file1
1041 self.create_file(&test_file_1, create_opts).await?;
1042
1043 // Now check whether it's possible to create file2
1044 let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
1045 Ok(_) => Ok(true),
1046 Err(e) => {
1047 if let Some(io_error) = e.downcast_ref::<io::Error>() {
1048 if io_error.kind() == io::ErrorKind::AlreadyExists {
1049 Ok(false)
1050 } else {
1051 Err(e)
1052 }
1053 } else {
1054 Err(e)
1055 }
1056 }
1057 };
1058
1059 temp_dir.close()?;
1060 case_sensitive
1061 }
1062}
1063
1064#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
1065impl Watcher for RealWatcher {
1066 fn add(&self, _: &Path) -> Result<()> {
1067 Ok(())
1068 }
1069
1070 fn remove(&self, _: &Path) -> Result<()> {
1071 Ok(())
1072 }
1073}
1074
1075#[cfg(any(test, feature = "test-support"))]
1076pub struct FakeFs {
1077 this: std::sync::Weak<Self>,
1078 // Use an unfair lock to ensure tests are deterministic.
1079 state: Arc<Mutex<FakeFsState>>,
1080 executor: gpui::BackgroundExecutor,
1081}
1082
1083#[cfg(any(test, feature = "test-support"))]
1084struct FakeFsState {
1085 root: FakeFsEntry,
1086 next_inode: u64,
1087 next_mtime: SystemTime,
1088 git_event_tx: smol::channel::Sender<PathBuf>,
1089 event_txs: Vec<(PathBuf, smol::channel::Sender<Vec<PathEvent>>)>,
1090 events_paused: bool,
1091 buffered_events: Vec<PathEvent>,
1092 metadata_call_count: usize,
1093 read_dir_call_count: usize,
1094 path_write_counts: std::collections::HashMap<PathBuf, usize>,
1095 moves: std::collections::HashMap<u64, PathBuf>,
1096}
1097
1098#[cfg(any(test, feature = "test-support"))]
1099#[derive(Clone, Debug)]
1100enum FakeFsEntry {
1101 File {
1102 inode: u64,
1103 mtime: MTime,
1104 len: u64,
1105 content: Vec<u8>,
1106 // The path to the repository state directory, if this is a gitfile.
1107 git_dir_path: Option<PathBuf>,
1108 },
1109 Dir {
1110 inode: u64,
1111 mtime: MTime,
1112 len: u64,
1113 entries: BTreeMap<String, FakeFsEntry>,
1114 git_repo_state: Option<Arc<Mutex<FakeGitRepositoryState>>>,
1115 },
1116 Symlink {
1117 target: PathBuf,
1118 },
1119}
1120
1121#[cfg(any(test, feature = "test-support"))]
1122impl PartialEq for FakeFsEntry {
1123 fn eq(&self, other: &Self) -> bool {
1124 match (self, other) {
1125 (
1126 Self::File {
1127 inode: l_inode,
1128 mtime: l_mtime,
1129 len: l_len,
1130 content: l_content,
1131 git_dir_path: l_git_dir_path,
1132 },
1133 Self::File {
1134 inode: r_inode,
1135 mtime: r_mtime,
1136 len: r_len,
1137 content: r_content,
1138 git_dir_path: r_git_dir_path,
1139 },
1140 ) => {
1141 l_inode == r_inode
1142 && l_mtime == r_mtime
1143 && l_len == r_len
1144 && l_content == r_content
1145 && l_git_dir_path == r_git_dir_path
1146 }
1147 (
1148 Self::Dir {
1149 inode: l_inode,
1150 mtime: l_mtime,
1151 len: l_len,
1152 entries: l_entries,
1153 git_repo_state: l_git_repo_state,
1154 },
1155 Self::Dir {
1156 inode: r_inode,
1157 mtime: r_mtime,
1158 len: r_len,
1159 entries: r_entries,
1160 git_repo_state: r_git_repo_state,
1161 },
1162 ) => {
1163 let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) {
1164 (Some(l), Some(r)) => Arc::ptr_eq(l, r),
1165 (None, None) => true,
1166 _ => false,
1167 };
1168 l_inode == r_inode
1169 && l_mtime == r_mtime
1170 && l_len == r_len
1171 && l_entries == r_entries
1172 && same_repo_state
1173 }
1174 (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => {
1175 l_target == r_target
1176 }
1177 _ => false,
1178 }
1179 }
1180}
1181
1182#[cfg(any(test, feature = "test-support"))]
1183impl FakeFsState {
1184 fn get_and_increment_mtime(&mut self) -> MTime {
1185 let mtime = self.next_mtime;
1186 self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
1187 MTime(mtime)
1188 }
1189
1190 fn get_and_increment_inode(&mut self) -> u64 {
1191 let inode = self.next_inode;
1192 self.next_inode += 1;
1193 inode
1194 }
1195
1196 fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option<PathBuf> {
1197 let mut canonical_path = PathBuf::new();
1198 let mut path = target.to_path_buf();
1199 let mut entry_stack = Vec::new();
1200 'outer: loop {
1201 let mut path_components = path.components().peekable();
1202 let mut prefix = None;
1203 while let Some(component) = path_components.next() {
1204 match component {
1205 Component::Prefix(prefix_component) => prefix = Some(prefix_component),
1206 Component::RootDir => {
1207 entry_stack.clear();
1208 entry_stack.push(&self.root);
1209 canonical_path.clear();
1210 match prefix {
1211 Some(prefix_component) => {
1212 canonical_path = PathBuf::from(prefix_component.as_os_str());
1213 // Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
1214 canonical_path.push(std::path::MAIN_SEPARATOR_STR);
1215 }
1216 None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
1217 }
1218 }
1219 Component::CurDir => {}
1220 Component::ParentDir => {
1221 entry_stack.pop()?;
1222 canonical_path.pop();
1223 }
1224 Component::Normal(name) => {
1225 let current_entry = *entry_stack.last()?;
1226 if let FakeFsEntry::Dir { entries, .. } = current_entry {
1227 let entry = entries.get(name.to_str().unwrap())?;
1228 if (path_components.peek().is_some() || follow_symlink)
1229 && let FakeFsEntry::Symlink { target, .. } = entry
1230 {
1231 let mut target = target.clone();
1232 target.extend(path_components);
1233 path = target;
1234 continue 'outer;
1235 }
1236 entry_stack.push(entry);
1237 canonical_path = canonical_path.join(name);
1238 } else {
1239 return None;
1240 }
1241 }
1242 }
1243 }
1244 break;
1245 }
1246
1247 if entry_stack.is_empty() {
1248 None
1249 } else {
1250 Some(canonical_path)
1251 }
1252 }
1253
1254 fn try_entry(
1255 &mut self,
1256 target: &Path,
1257 follow_symlink: bool,
1258 ) -> Option<(&mut FakeFsEntry, PathBuf)> {
1259 let canonical_path = self.canonicalize(target, follow_symlink)?;
1260
1261 let mut components = canonical_path
1262 .components()
1263 .skip_while(|component| matches!(component, Component::Prefix(_)));
1264 let Some(Component::RootDir) = components.next() else {
1265 panic!(
1266 "the path {:?} was not canonicalized properly {:?}",
1267 target, canonical_path
1268 )
1269 };
1270
1271 let mut entry = &mut self.root;
1272 for component in components {
1273 match component {
1274 Component::Normal(name) => {
1275 if let FakeFsEntry::Dir { entries, .. } = entry {
1276 entry = entries.get_mut(name.to_str().unwrap())?;
1277 } else {
1278 return None;
1279 }
1280 }
1281 _ => {
1282 panic!(
1283 "the path {:?} was not canonicalized properly {:?}",
1284 target, canonical_path
1285 )
1286 }
1287 }
1288 }
1289
1290 Some((entry, canonical_path))
1291 }
1292
1293 fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> {
1294 Ok(self
1295 .try_entry(target, true)
1296 .ok_or_else(|| {
1297 anyhow!(io::Error::new(
1298 io::ErrorKind::NotFound,
1299 format!("not found: {target:?}")
1300 ))
1301 })?
1302 .0)
1303 }
1304
1305 fn write_path<Fn, T>(&mut self, path: &Path, callback: Fn) -> Result<T>
1306 where
1307 Fn: FnOnce(btree_map::Entry<String, FakeFsEntry>) -> Result<T>,
1308 {
1309 let path = normalize_path(path);
1310 let filename = path.file_name().context("cannot overwrite the root")?;
1311 let parent_path = path.parent().unwrap();
1312
1313 let parent = self.entry(parent_path)?;
1314 let new_entry = parent
1315 .dir_entries(parent_path)?
1316 .entry(filename.to_str().unwrap().into());
1317 callback(new_entry)
1318 }
1319
1320 fn emit_event<I, T>(&mut self, paths: I)
1321 where
1322 I: IntoIterator<Item = (T, Option<PathEventKind>)>,
1323 T: Into<PathBuf>,
1324 {
1325 self.buffered_events
1326 .extend(paths.into_iter().map(|(path, kind)| PathEvent {
1327 path: path.into(),
1328 kind,
1329 }));
1330
1331 if !self.events_paused {
1332 self.flush_events(self.buffered_events.len());
1333 }
1334 }
1335
1336 fn flush_events(&mut self, mut count: usize) {
1337 count = count.min(self.buffered_events.len());
1338 let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
1339 self.event_txs.retain(|(_, tx)| {
1340 let _ = tx.try_send(events.clone());
1341 !tx.is_closed()
1342 });
1343 }
1344}
1345
1346#[cfg(any(test, feature = "test-support"))]
1347pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
1348 std::sync::LazyLock::new(|| OsStr::new(".git"));
1349
1350#[cfg(any(test, feature = "test-support"))]
1351impl FakeFs {
1352 /// We need to use something large enough for Windows and Unix to consider this a new file.
1353 /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
1354 const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
1355
1356 pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
1357 let (tx, rx) = smol::channel::bounded::<PathBuf>(10);
1358
1359 let this = Arc::new_cyclic(|this| Self {
1360 this: this.clone(),
1361 executor: executor.clone(),
1362 state: Arc::new(Mutex::new(FakeFsState {
1363 root: FakeFsEntry::Dir {
1364 inode: 0,
1365 mtime: MTime(UNIX_EPOCH),
1366 len: 0,
1367 entries: Default::default(),
1368 git_repo_state: None,
1369 },
1370 git_event_tx: tx,
1371 next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
1372 next_inode: 1,
1373 event_txs: Default::default(),
1374 buffered_events: Vec::new(),
1375 events_paused: false,
1376 read_dir_call_count: 0,
1377 metadata_call_count: 0,
1378 path_write_counts: Default::default(),
1379 moves: Default::default(),
1380 })),
1381 });
1382
1383 executor.spawn({
1384 let this = this.clone();
1385 async move {
1386 while let Ok(git_event) = rx.recv().await {
1387 if let Some(mut state) = this.state.try_lock() {
1388 state.emit_event([(git_event, Some(PathEventKind::Changed))]);
1389 } else {
1390 panic!("Failed to lock file system state, this execution would have caused a test hang");
1391 }
1392 }
1393 }
1394 }).detach();
1395
1396 this
1397 }
1398
1399 pub fn set_next_mtime(&self, next_mtime: SystemTime) {
1400 let mut state = self.state.lock();
1401 state.next_mtime = next_mtime;
1402 }
1403
1404 pub fn get_and_increment_mtime(&self) -> MTime {
1405 let mut state = self.state.lock();
1406 state.get_and_increment_mtime()
1407 }
1408
1409 pub async fn touch_path(&self, path: impl AsRef<Path>) {
1410 let mut state = self.state.lock();
1411 let path = path.as_ref();
1412 let new_mtime = state.get_and_increment_mtime();
1413 let new_inode = state.get_and_increment_inode();
1414 state
1415 .write_path(path, move |entry| {
1416 match entry {
1417 btree_map::Entry::Vacant(e) => {
1418 e.insert(FakeFsEntry::File {
1419 inode: new_inode,
1420 mtime: new_mtime,
1421 content: Vec::new(),
1422 len: 0,
1423 git_dir_path: None,
1424 });
1425 }
1426 btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() {
1427 FakeFsEntry::File { mtime, .. } => *mtime = new_mtime,
1428 FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime,
1429 FakeFsEntry::Symlink { .. } => {}
1430 },
1431 }
1432 Ok(())
1433 })
1434 .unwrap();
1435 state.emit_event([(path.to_path_buf(), Some(PathEventKind::Changed))]);
1436 }
1437
1438 pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
1439 self.write_file_internal(path, content, true).unwrap()
1440 }
1441
1442 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
1443 let mut state = self.state.lock();
1444 let path = path.as_ref();
1445 let file = FakeFsEntry::Symlink { target };
1446 state
1447 .write_path(path.as_ref(), move |e| match e {
1448 btree_map::Entry::Vacant(e) => {
1449 e.insert(file);
1450 Ok(())
1451 }
1452 btree_map::Entry::Occupied(mut e) => {
1453 *e.get_mut() = file;
1454 Ok(())
1455 }
1456 })
1457 .unwrap();
1458 state.emit_event([(path, Some(PathEventKind::Created))]);
1459 }
1460
1461 fn write_file_internal(
1462 &self,
1463 path: impl AsRef<Path>,
1464 new_content: Vec<u8>,
1465 recreate_inode: bool,
1466 ) -> Result<()> {
1467 let mut state = self.state.lock();
1468 let path_buf = path.as_ref().to_path_buf();
1469 *state.path_write_counts.entry(path_buf).or_insert(0) += 1;
1470 let new_inode = state.get_and_increment_inode();
1471 let new_mtime = state.get_and_increment_mtime();
1472 let new_len = new_content.len() as u64;
1473 let mut kind = None;
1474 state.write_path(path.as_ref(), |entry| {
1475 match entry {
1476 btree_map::Entry::Vacant(e) => {
1477 kind = Some(PathEventKind::Created);
1478 e.insert(FakeFsEntry::File {
1479 inode: new_inode,
1480 mtime: new_mtime,
1481 len: new_len,
1482 content: new_content,
1483 git_dir_path: None,
1484 });
1485 }
1486 btree_map::Entry::Occupied(mut e) => {
1487 kind = Some(PathEventKind::Changed);
1488 if let FakeFsEntry::File {
1489 inode,
1490 mtime,
1491 len,
1492 content,
1493 ..
1494 } = e.get_mut()
1495 {
1496 *mtime = new_mtime;
1497 *content = new_content;
1498 *len = new_len;
1499 if recreate_inode {
1500 *inode = new_inode;
1501 }
1502 } else {
1503 anyhow::bail!("not a file")
1504 }
1505 }
1506 }
1507 Ok(())
1508 })?;
1509 state.emit_event([(path.as_ref(), kind)]);
1510 Ok(())
1511 }
1512
1513 pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1514 let path = path.as_ref();
1515 let path = normalize_path(path);
1516 let mut state = self.state.lock();
1517 let entry = state.entry(&path)?;
1518 entry.file_content(&path).cloned()
1519 }
1520
1521 async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1522 let path = path.as_ref();
1523 let path = normalize_path(path);
1524 self.simulate_random_delay().await;
1525 let mut state = self.state.lock();
1526 let entry = state.entry(&path)?;
1527 entry.file_content(&path).cloned()
1528 }
1529
1530 pub fn pause_events(&self) {
1531 self.state.lock().events_paused = true;
1532 }
1533
1534 pub fn unpause_events_and_flush(&self) {
1535 self.state.lock().events_paused = false;
1536 self.flush_events(usize::MAX);
1537 }
1538
1539 pub fn buffered_event_count(&self) -> usize {
1540 self.state.lock().buffered_events.len()
1541 }
1542
1543 pub fn flush_events(&self, count: usize) {
1544 self.state.lock().flush_events(count);
1545 }
1546
1547 pub(crate) fn entry(&self, target: &Path) -> Result<FakeFsEntry> {
1548 self.state.lock().entry(target).cloned()
1549 }
1550
1551 pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> {
1552 let mut state = self.state.lock();
1553 state.write_path(target, |entry| {
1554 match entry {
1555 btree_map::Entry::Vacant(vacant_entry) => {
1556 vacant_entry.insert(new_entry);
1557 }
1558 btree_map::Entry::Occupied(mut occupied_entry) => {
1559 occupied_entry.insert(new_entry);
1560 }
1561 }
1562 Ok(())
1563 })
1564 }
1565
1566 #[must_use]
1567 pub fn insert_tree<'a>(
1568 &'a self,
1569 path: impl 'a + AsRef<Path> + Send,
1570 tree: serde_json::Value,
1571 ) -> futures::future::BoxFuture<'a, ()> {
1572 use futures::FutureExt as _;
1573 use serde_json::Value::*;
1574
1575 async move {
1576 let path = path.as_ref();
1577
1578 match tree {
1579 Object(map) => {
1580 self.create_dir(path).await.unwrap();
1581 for (name, contents) in map {
1582 let mut path = PathBuf::from(path);
1583 path.push(name);
1584 self.insert_tree(&path, contents).await;
1585 }
1586 }
1587 Null => {
1588 self.create_dir(path).await.unwrap();
1589 }
1590 String(contents) => {
1591 self.insert_file(&path, contents.into_bytes()).await;
1592 }
1593 _ => {
1594 panic!("JSON object must contain only objects, strings, or null");
1595 }
1596 }
1597 }
1598 .boxed()
1599 }
1600
1601 pub fn insert_tree_from_real_fs<'a>(
1602 &'a self,
1603 path: impl 'a + AsRef<Path> + Send,
1604 src_path: impl 'a + AsRef<Path> + Send,
1605 ) -> futures::future::BoxFuture<'a, ()> {
1606 use futures::FutureExt as _;
1607
1608 async move {
1609 let path = path.as_ref();
1610 if std::fs::metadata(&src_path).unwrap().is_file() {
1611 let contents = std::fs::read(src_path).unwrap();
1612 self.insert_file(path, contents).await;
1613 } else {
1614 self.create_dir(path).await.unwrap();
1615 for entry in std::fs::read_dir(&src_path).unwrap() {
1616 let entry = entry.unwrap();
1617 self.insert_tree_from_real_fs(path.join(entry.file_name()), entry.path())
1618 .await;
1619 }
1620 }
1621 }
1622 .boxed()
1623 }
1624
1625 pub fn with_git_state_and_paths<T, F>(
1626 &self,
1627 dot_git: &Path,
1628 emit_git_event: bool,
1629 f: F,
1630 ) -> Result<T>
1631 where
1632 F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T,
1633 {
1634 let mut state = self.state.lock();
1635 let git_event_tx = state.git_event_tx.clone();
1636 let entry = state.entry(dot_git).context("open .git")?;
1637
1638 if let FakeFsEntry::Dir { git_repo_state, .. } = entry {
1639 let repo_state = git_repo_state.get_or_insert_with(|| {
1640 log::debug!("insert git state for {dot_git:?}");
1641 Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
1642 });
1643 let mut repo_state = repo_state.lock();
1644
1645 let result = f(&mut repo_state, dot_git, dot_git);
1646
1647 drop(repo_state);
1648 if emit_git_event {
1649 state.emit_event([(dot_git, Some(PathEventKind::Changed))]);
1650 }
1651
1652 Ok(result)
1653 } else if let FakeFsEntry::File {
1654 content,
1655 git_dir_path,
1656 ..
1657 } = &mut *entry
1658 {
1659 let path = match git_dir_path {
1660 Some(path) => path,
1661 None => {
1662 let path = std::str::from_utf8(content)
1663 .ok()
1664 .and_then(|content| content.strip_prefix("gitdir:"))
1665 .context("not a valid gitfile")?
1666 .trim();
1667 git_dir_path.insert(normalize_path(&dot_git.parent().unwrap().join(path)))
1668 }
1669 }
1670 .clone();
1671 let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else {
1672 anyhow::bail!("pointed-to git dir {path:?} not found")
1673 };
1674 let FakeFsEntry::Dir {
1675 git_repo_state,
1676 entries,
1677 ..
1678 } = git_dir_entry
1679 else {
1680 anyhow::bail!("gitfile points to a non-directory")
1681 };
1682 let common_dir = if let Some(child) = entries.get("commondir") {
1683 Path::new(
1684 std::str::from_utf8(child.file_content("commondir".as_ref())?)
1685 .context("commondir content")?,
1686 )
1687 .to_owned()
1688 } else {
1689 canonical_path.clone()
1690 };
1691 let repo_state = git_repo_state.get_or_insert_with(|| {
1692 Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
1693 });
1694 let mut repo_state = repo_state.lock();
1695
1696 let result = f(&mut repo_state, &canonical_path, &common_dir);
1697
1698 if emit_git_event {
1699 drop(repo_state);
1700 state.emit_event([(canonical_path, Some(PathEventKind::Changed))]);
1701 }
1702
1703 Ok(result)
1704 } else {
1705 anyhow::bail!("not a valid git repository");
1706 }
1707 }
1708
1709 pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
1710 where
1711 F: FnOnce(&mut FakeGitRepositoryState) -> T,
1712 {
1713 self.with_git_state_and_paths(dot_git, emit_git_event, |state, _, _| f(state))
1714 }
1715
1716 pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
1717 self.with_git_state(dot_git, true, |state| {
1718 let branch = branch.map(Into::into);
1719 state.branches.extend(branch.clone());
1720 state.current_branch_name = branch
1721 })
1722 .unwrap();
1723 }
1724
1725 pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
1726 self.with_git_state(dot_git, true, |state| {
1727 if let Some(first) = branches.first()
1728 && state.current_branch_name.is_none()
1729 {
1730 state.current_branch_name = Some(first.to_string())
1731 }
1732 state
1733 .branches
1734 .extend(branches.iter().map(ToString::to_string));
1735 })
1736 .unwrap();
1737 }
1738
1739 pub fn set_unmerged_paths_for_repo(
1740 &self,
1741 dot_git: &Path,
1742 unmerged_state: &[(RepoPath, UnmergedStatus)],
1743 ) {
1744 self.with_git_state(dot_git, true, |state| {
1745 state.unmerged_paths.clear();
1746 state.unmerged_paths.extend(
1747 unmerged_state
1748 .iter()
1749 .map(|(path, content)| (path.clone(), *content)),
1750 );
1751 })
1752 .unwrap();
1753 }
1754
1755 pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(&str, String)]) {
1756 self.with_git_state(dot_git, true, |state| {
1757 state.index_contents.clear();
1758 state.index_contents.extend(
1759 index_state
1760 .iter()
1761 .map(|(path, content)| (repo_path(path), content.clone())),
1762 );
1763 })
1764 .unwrap();
1765 }
1766
1767 pub fn set_head_for_repo(
1768 &self,
1769 dot_git: &Path,
1770 head_state: &[(&str, String)],
1771 sha: impl Into<String>,
1772 ) {
1773 self.with_git_state(dot_git, true, |state| {
1774 state.head_contents.clear();
1775 state.head_contents.extend(
1776 head_state
1777 .iter()
1778 .map(|(path, content)| (repo_path(path), content.clone())),
1779 );
1780 state.refs.insert("HEAD".into(), sha.into());
1781 })
1782 .unwrap();
1783 }
1784
1785 pub fn set_head_and_index_for_repo(&self, dot_git: &Path, contents_by_path: &[(&str, String)]) {
1786 self.with_git_state(dot_git, true, |state| {
1787 state.head_contents.clear();
1788 state.head_contents.extend(
1789 contents_by_path
1790 .iter()
1791 .map(|(path, contents)| (repo_path(path), contents.clone())),
1792 );
1793 state.index_contents = state.head_contents.clone();
1794 })
1795 .unwrap();
1796 }
1797
1798 pub fn set_merge_base_content_for_repo(
1799 &self,
1800 dot_git: &Path,
1801 contents_by_path: &[(&str, String)],
1802 ) {
1803 self.with_git_state(dot_git, true, |state| {
1804 use git::Oid;
1805
1806 state.merge_base_contents.clear();
1807 let oids = (1..)
1808 .map(|n| n.to_string())
1809 .map(|n| Oid::from_bytes(n.repeat(20).as_bytes()).unwrap());
1810 for ((path, content), oid) in contents_by_path.iter().zip(oids) {
1811 state.merge_base_contents.insert(repo_path(path), oid);
1812 state.oids.insert(oid, content.clone());
1813 }
1814 })
1815 .unwrap();
1816 }
1817
1818 pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
1819 self.with_git_state(dot_git, true, |state| {
1820 state.blames.clear();
1821 state.blames.extend(blames);
1822 })
1823 .unwrap();
1824 }
1825
1826 /// Put the given git repository into a state with the given status,
1827 /// by mutating the head, index, and unmerged state.
1828 pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&str, FileStatus)]) {
1829 let workdir_path = dot_git.parent().unwrap();
1830 let workdir_contents = self.files_with_contents(workdir_path);
1831 self.with_git_state(dot_git, true, |state| {
1832 state.index_contents.clear();
1833 state.head_contents.clear();
1834 state.unmerged_paths.clear();
1835 for (path, content) in workdir_contents {
1836 use util::{paths::PathStyle, rel_path::RelPath};
1837
1838 let repo_path: RepoPath = RelPath::new(path.strip_prefix(&workdir_path).unwrap(), PathStyle::local()).unwrap().into();
1839 let status = statuses
1840 .iter()
1841 .find_map(|(p, status)| (*p == repo_path.as_unix_str()).then_some(status));
1842 let mut content = String::from_utf8_lossy(&content).to_string();
1843
1844 let mut index_content = None;
1845 let mut head_content = None;
1846 match status {
1847 None => {
1848 index_content = Some(content.clone());
1849 head_content = Some(content);
1850 }
1851 Some(FileStatus::Untracked | FileStatus::Ignored) => {}
1852 Some(FileStatus::Unmerged(unmerged_status)) => {
1853 state
1854 .unmerged_paths
1855 .insert(repo_path.clone(), *unmerged_status);
1856 content.push_str(" (unmerged)");
1857 index_content = Some(content.clone());
1858 head_content = Some(content);
1859 }
1860 Some(FileStatus::Tracked(TrackedStatus {
1861 index_status,
1862 worktree_status,
1863 })) => {
1864 match worktree_status {
1865 StatusCode::Modified => {
1866 let mut content = content.clone();
1867 content.push_str(" (modified in working copy)");
1868 index_content = Some(content);
1869 }
1870 StatusCode::TypeChanged | StatusCode::Unmodified => {
1871 index_content = Some(content.clone());
1872 }
1873 StatusCode::Added => {}
1874 StatusCode::Deleted | StatusCode::Renamed | StatusCode::Copied => {
1875 panic!("cannot create these statuses for an existing file");
1876 }
1877 };
1878 match index_status {
1879 StatusCode::Modified => {
1880 let mut content = index_content.clone().expect(
1881 "file cannot be both modified in index and created in working copy",
1882 );
1883 content.push_str(" (modified in index)");
1884 head_content = Some(content);
1885 }
1886 StatusCode::TypeChanged | StatusCode::Unmodified => {
1887 head_content = Some(index_content.clone().expect("file cannot be both unmodified in index and created in working copy"));
1888 }
1889 StatusCode::Added => {}
1890 StatusCode::Deleted => {
1891 head_content = Some("".into());
1892 }
1893 StatusCode::Renamed | StatusCode::Copied => {
1894 panic!("cannot create these statuses for an existing file");
1895 }
1896 };
1897 }
1898 };
1899
1900 if let Some(content) = index_content {
1901 state.index_contents.insert(repo_path.clone(), content);
1902 }
1903 if let Some(content) = head_content {
1904 state.head_contents.insert(repo_path.clone(), content);
1905 }
1906 }
1907 }).unwrap();
1908 }
1909
1910 pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
1911 self.with_git_state(dot_git, true, |state| {
1912 state.simulated_index_write_error_message = message;
1913 })
1914 .unwrap();
1915 }
1916
1917 pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
1918 let mut result = Vec::new();
1919 let mut queue = collections::VecDeque::new();
1920 let state = &*self.state.lock();
1921 queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1922 while let Some((path, entry)) = queue.pop_front() {
1923 if let FakeFsEntry::Dir { entries, .. } = entry {
1924 for (name, entry) in entries {
1925 queue.push_back((path.join(name), entry));
1926 }
1927 }
1928 if include_dot_git
1929 || !path
1930 .components()
1931 .any(|component| component.as_os_str() == *FS_DOT_GIT)
1932 {
1933 result.push(path);
1934 }
1935 }
1936 result
1937 }
1938
1939 pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
1940 let mut result = Vec::new();
1941 let mut queue = collections::VecDeque::new();
1942 let state = &*self.state.lock();
1943 queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1944 while let Some((path, entry)) = queue.pop_front() {
1945 if let FakeFsEntry::Dir { entries, .. } = entry {
1946 for (name, entry) in entries {
1947 queue.push_back((path.join(name), entry));
1948 }
1949 if include_dot_git
1950 || !path
1951 .components()
1952 .any(|component| component.as_os_str() == *FS_DOT_GIT)
1953 {
1954 result.push(path);
1955 }
1956 }
1957 }
1958 result
1959 }
1960
1961 pub fn files(&self) -> Vec<PathBuf> {
1962 let mut result = Vec::new();
1963 let mut queue = collections::VecDeque::new();
1964 let state = &*self.state.lock();
1965 queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1966 while let Some((path, entry)) = queue.pop_front() {
1967 match entry {
1968 FakeFsEntry::File { .. } => result.push(path),
1969 FakeFsEntry::Dir { entries, .. } => {
1970 for (name, entry) in entries {
1971 queue.push_back((path.join(name), entry));
1972 }
1973 }
1974 FakeFsEntry::Symlink { .. } => {}
1975 }
1976 }
1977 result
1978 }
1979
1980 pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec<u8>)> {
1981 let mut result = Vec::new();
1982 let mut queue = collections::VecDeque::new();
1983 let state = &*self.state.lock();
1984 queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1985 while let Some((path, entry)) = queue.pop_front() {
1986 match entry {
1987 FakeFsEntry::File { content, .. } => {
1988 if path.starts_with(prefix) {
1989 result.push((path, content.clone()));
1990 }
1991 }
1992 FakeFsEntry::Dir { entries, .. } => {
1993 for (name, entry) in entries {
1994 queue.push_back((path.join(name), entry));
1995 }
1996 }
1997 FakeFsEntry::Symlink { .. } => {}
1998 }
1999 }
2000 result
2001 }
2002
2003 /// How many `read_dir` calls have been issued.
2004 pub fn read_dir_call_count(&self) -> usize {
2005 self.state.lock().read_dir_call_count
2006 }
2007
2008 pub fn watched_paths(&self) -> Vec<PathBuf> {
2009 let state = self.state.lock();
2010 state
2011 .event_txs
2012 .iter()
2013 .filter_map(|(path, tx)| Some(path.clone()).filter(|_| !tx.is_closed()))
2014 .collect()
2015 }
2016
2017 /// How many `metadata` calls have been issued.
2018 pub fn metadata_call_count(&self) -> usize {
2019 self.state.lock().metadata_call_count
2020 }
2021
2022 /// How many write operations have been issued for a specific path.
2023 pub fn write_count_for_path(&self, path: impl AsRef<Path>) -> usize {
2024 let path = path.as_ref().to_path_buf();
2025 self.state
2026 .lock()
2027 .path_write_counts
2028 .get(&path)
2029 .copied()
2030 .unwrap_or(0)
2031 }
2032
2033 pub fn emit_fs_event(&self, path: impl Into<PathBuf>, event: Option<PathEventKind>) {
2034 self.state.lock().emit_event(std::iter::once((path, event)));
2035 }
2036
2037 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
2038 self.executor.simulate_random_delay()
2039 }
2040}
2041
2042#[cfg(any(test, feature = "test-support"))]
2043impl FakeFsEntry {
2044 fn is_file(&self) -> bool {
2045 matches!(self, Self::File { .. })
2046 }
2047
2048 fn is_symlink(&self) -> bool {
2049 matches!(self, Self::Symlink { .. })
2050 }
2051
2052 fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
2053 if let Self::File { content, .. } = self {
2054 Ok(content)
2055 } else {
2056 anyhow::bail!("not a file: {path:?}");
2057 }
2058 }
2059
2060 fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap<String, FakeFsEntry>> {
2061 if let Self::Dir { entries, .. } = self {
2062 Ok(entries)
2063 } else {
2064 anyhow::bail!("not a directory: {path:?}");
2065 }
2066 }
2067}
2068
2069#[cfg(any(test, feature = "test-support"))]
2070struct FakeWatcher {
2071 tx: smol::channel::Sender<Vec<PathEvent>>,
2072 original_path: PathBuf,
2073 fs_state: Arc<Mutex<FakeFsState>>,
2074 prefixes: Mutex<Vec<PathBuf>>,
2075}
2076
2077#[cfg(any(test, feature = "test-support"))]
2078impl Watcher for FakeWatcher {
2079 fn add(&self, path: &Path) -> Result<()> {
2080 if path.starts_with(&self.original_path) {
2081 return Ok(());
2082 }
2083 self.fs_state
2084 .try_lock()
2085 .unwrap()
2086 .event_txs
2087 .push((path.to_owned(), self.tx.clone()));
2088 self.prefixes.lock().push(path.to_owned());
2089 Ok(())
2090 }
2091
2092 fn remove(&self, _: &Path) -> Result<()> {
2093 Ok(())
2094 }
2095}
2096
2097#[cfg(any(test, feature = "test-support"))]
2098#[derive(Debug)]
2099struct FakeHandle {
2100 inode: u64,
2101}
2102
2103#[cfg(any(test, feature = "test-support"))]
2104impl FileHandle for FakeHandle {
2105 fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
2106 let fs = fs.as_fake();
2107 let mut state = fs.state.lock();
2108 let Some(target) = state.moves.get(&self.inode).cloned() else {
2109 anyhow::bail!("fake fd not moved")
2110 };
2111
2112 if state.try_entry(&target, false).is_some() {
2113 return Ok(target);
2114 }
2115 anyhow::bail!("fake fd target not found")
2116 }
2117}
2118
2119#[cfg(any(test, feature = "test-support"))]
2120#[async_trait::async_trait]
2121impl Fs for FakeFs {
2122 async fn create_dir(&self, path: &Path) -> Result<()> {
2123 self.simulate_random_delay().await;
2124
2125 let mut created_dirs = Vec::new();
2126 let mut cur_path = PathBuf::new();
2127 for component in path.components() {
2128 let should_skip = matches!(component, Component::Prefix(..) | Component::RootDir);
2129 cur_path.push(component);
2130 if should_skip {
2131 continue;
2132 }
2133 let mut state = self.state.lock();
2134
2135 let inode = state.get_and_increment_inode();
2136 let mtime = state.get_and_increment_mtime();
2137 state.write_path(&cur_path, |entry| {
2138 entry.or_insert_with(|| {
2139 created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
2140 FakeFsEntry::Dir {
2141 inode,
2142 mtime,
2143 len: 0,
2144 entries: Default::default(),
2145 git_repo_state: None,
2146 }
2147 });
2148 Ok(())
2149 })?
2150 }
2151
2152 self.state.lock().emit_event(created_dirs);
2153 Ok(())
2154 }
2155
2156 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
2157 self.simulate_random_delay().await;
2158 let mut state = self.state.lock();
2159 let inode = state.get_and_increment_inode();
2160 let mtime = state.get_and_increment_mtime();
2161 let file = FakeFsEntry::File {
2162 inode,
2163 mtime,
2164 len: 0,
2165 content: Vec::new(),
2166 git_dir_path: None,
2167 };
2168 let mut kind = Some(PathEventKind::Created);
2169 state.write_path(path, |entry| {
2170 match entry {
2171 btree_map::Entry::Occupied(mut e) => {
2172 if options.overwrite {
2173 kind = Some(PathEventKind::Changed);
2174 *e.get_mut() = file;
2175 } else if !options.ignore_if_exists {
2176 anyhow::bail!("path already exists: {path:?}");
2177 }
2178 }
2179 btree_map::Entry::Vacant(e) => {
2180 e.insert(file);
2181 }
2182 }
2183 Ok(())
2184 })?;
2185 state.emit_event([(path, kind)]);
2186 Ok(())
2187 }
2188
2189 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
2190 let mut state = self.state.lock();
2191 let file = FakeFsEntry::Symlink { target };
2192 state
2193 .write_path(path.as_ref(), move |e| match e {
2194 btree_map::Entry::Vacant(e) => {
2195 e.insert(file);
2196 Ok(())
2197 }
2198 btree_map::Entry::Occupied(mut e) => {
2199 *e.get_mut() = file;
2200 Ok(())
2201 }
2202 })
2203 .unwrap();
2204 state.emit_event([(path, Some(PathEventKind::Created))]);
2205
2206 Ok(())
2207 }
2208
2209 async fn create_file_with(
2210 &self,
2211 path: &Path,
2212 mut content: Pin<&mut (dyn AsyncRead + Send)>,
2213 ) -> Result<()> {
2214 let mut bytes = Vec::new();
2215 content.read_to_end(&mut bytes).await?;
2216 self.write_file_internal(path, bytes, true)?;
2217 Ok(())
2218 }
2219
2220 async fn extract_tar_file(
2221 &self,
2222 path: &Path,
2223 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
2224 ) -> Result<()> {
2225 let mut entries = content.entries()?;
2226 while let Some(entry) = entries.next().await {
2227 let mut entry = entry?;
2228 if entry.header().entry_type().is_file() {
2229 let path = path.join(entry.path()?.as_ref());
2230 let mut bytes = Vec::new();
2231 entry.read_to_end(&mut bytes).await?;
2232 self.create_dir(path.parent().unwrap()).await?;
2233 self.write_file_internal(&path, bytes, true)?;
2234 }
2235 }
2236 Ok(())
2237 }
2238
2239 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
2240 self.simulate_random_delay().await;
2241
2242 let old_path = normalize_path(old_path);
2243 let new_path = normalize_path(new_path);
2244
2245 let mut state = self.state.lock();
2246 let moved_entry = state.write_path(&old_path, |e| {
2247 if let btree_map::Entry::Occupied(e) = e {
2248 Ok(e.get().clone())
2249 } else {
2250 anyhow::bail!("path does not exist: {old_path:?}")
2251 }
2252 })?;
2253
2254 let inode = match moved_entry {
2255 FakeFsEntry::File { inode, .. } => inode,
2256 FakeFsEntry::Dir { inode, .. } => inode,
2257 _ => 0,
2258 };
2259
2260 state.moves.insert(inode, new_path.clone());
2261
2262 state.write_path(&new_path, |e| {
2263 match e {
2264 btree_map::Entry::Occupied(mut e) => {
2265 if options.overwrite {
2266 *e.get_mut() = moved_entry;
2267 } else if !options.ignore_if_exists {
2268 anyhow::bail!("path already exists: {new_path:?}");
2269 }
2270 }
2271 btree_map::Entry::Vacant(e) => {
2272 e.insert(moved_entry);
2273 }
2274 }
2275 Ok(())
2276 })?;
2277
2278 state
2279 .write_path(&old_path, |e| {
2280 if let btree_map::Entry::Occupied(e) = e {
2281 Ok(e.remove())
2282 } else {
2283 unreachable!()
2284 }
2285 })
2286 .unwrap();
2287
2288 state.emit_event([
2289 (old_path, Some(PathEventKind::Removed)),
2290 (new_path, Some(PathEventKind::Created)),
2291 ]);
2292 Ok(())
2293 }
2294
2295 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
2296 self.simulate_random_delay().await;
2297
2298 let source = normalize_path(source);
2299 let target = normalize_path(target);
2300 let mut state = self.state.lock();
2301 let mtime = state.get_and_increment_mtime();
2302 let inode = state.get_and_increment_inode();
2303 let source_entry = state.entry(&source)?;
2304 let content = source_entry.file_content(&source)?.clone();
2305 let mut kind = Some(PathEventKind::Created);
2306 state.write_path(&target, |e| match e {
2307 btree_map::Entry::Occupied(e) => {
2308 if options.overwrite {
2309 kind = Some(PathEventKind::Changed);
2310 Ok(Some(e.get().clone()))
2311 } else if !options.ignore_if_exists {
2312 anyhow::bail!("{target:?} already exists");
2313 } else {
2314 Ok(None)
2315 }
2316 }
2317 btree_map::Entry::Vacant(e) => Ok(Some(
2318 e.insert(FakeFsEntry::File {
2319 inode,
2320 mtime,
2321 len: content.len() as u64,
2322 content,
2323 git_dir_path: None,
2324 })
2325 .clone(),
2326 )),
2327 })?;
2328 state.emit_event([(target, kind)]);
2329 Ok(())
2330 }
2331
2332 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
2333 self.simulate_random_delay().await;
2334
2335 let path = normalize_path(path);
2336 let parent_path = path.parent().context("cannot remove the root")?;
2337 let base_name = path.file_name().context("cannot remove the root")?;
2338
2339 let mut state = self.state.lock();
2340 let parent_entry = state.entry(parent_path)?;
2341 let entry = parent_entry
2342 .dir_entries(parent_path)?
2343 .entry(base_name.to_str().unwrap().into());
2344
2345 match entry {
2346 btree_map::Entry::Vacant(_) => {
2347 if !options.ignore_if_not_exists {
2348 anyhow::bail!("{path:?} does not exist");
2349 }
2350 }
2351 btree_map::Entry::Occupied(mut entry) => {
2352 {
2353 let children = entry.get_mut().dir_entries(&path)?;
2354 if !options.recursive && !children.is_empty() {
2355 anyhow::bail!("{path:?} is not empty");
2356 }
2357 }
2358 entry.remove();
2359 }
2360 }
2361 state.emit_event([(path, Some(PathEventKind::Removed))]);
2362 Ok(())
2363 }
2364
2365 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
2366 self.simulate_random_delay().await;
2367
2368 let path = normalize_path(path);
2369 let parent_path = path.parent().context("cannot remove the root")?;
2370 let base_name = path.file_name().unwrap();
2371 let mut state = self.state.lock();
2372 let parent_entry = state.entry(parent_path)?;
2373 let entry = parent_entry
2374 .dir_entries(parent_path)?
2375 .entry(base_name.to_str().unwrap().into());
2376 match entry {
2377 btree_map::Entry::Vacant(_) => {
2378 if !options.ignore_if_not_exists {
2379 anyhow::bail!("{path:?} does not exist");
2380 }
2381 }
2382 btree_map::Entry::Occupied(mut entry) => {
2383 entry.get_mut().file_content(&path)?;
2384 entry.remove();
2385 }
2386 }
2387 state.emit_event([(path, Some(PathEventKind::Removed))]);
2388 Ok(())
2389 }
2390
2391 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>> {
2392 let bytes = self.load_internal(path).await?;
2393 Ok(Box::new(io::Cursor::new(bytes)))
2394 }
2395
2396 async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
2397 self.simulate_random_delay().await;
2398 let mut state = self.state.lock();
2399 let inode = match state.entry(path)? {
2400 FakeFsEntry::File { inode, .. } => *inode,
2401 FakeFsEntry::Dir { inode, .. } => *inode,
2402 _ => unreachable!(),
2403 };
2404 Ok(Arc::new(FakeHandle { inode }))
2405 }
2406
2407 async fn load(&self, path: &Path) -> Result<String> {
2408 let content = self.load_internal(path).await?;
2409 Ok(String::from_utf8(content)?)
2410 }
2411
2412 async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
2413 self.load_internal(path).await
2414 }
2415
2416 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
2417 self.simulate_random_delay().await;
2418 let path = normalize_path(path.as_path());
2419 if let Some(path) = path.parent() {
2420 self.create_dir(path).await?;
2421 }
2422 self.write_file_internal(path, data.into_bytes(), true)?;
2423 Ok(())
2424 }
2425
2426 async fn save(
2427 &self,
2428 path: &Path,
2429 text: &Rope,
2430 line_ending: LineEnding,
2431 encoding: EncodingWrapper,
2432 ) -> Result<()> {
2433 use crate::encodings::from_utf8;
2434
2435 self.simulate_random_delay().await;
2436 let path = normalize_path(path);
2437 let content = chunks(text, line_ending).collect::<String>();
2438 if let Some(path) = path.parent() {
2439 self.create_dir(path).await?;
2440 }
2441 self.write_file_internal(path, from_utf8(content, encoding).await?, false)?;
2442 Ok(())
2443 }
2444
2445 async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
2446 self.simulate_random_delay().await;
2447 let path = normalize_path(path);
2448 if let Some(path) = path.parent() {
2449 self.create_dir(path).await?;
2450 }
2451 self.write_file_internal(path, content.to_vec(), false)?;
2452 Ok(())
2453 }
2454
2455 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
2456 let path = normalize_path(path);
2457 self.simulate_random_delay().await;
2458 let state = self.state.lock();
2459 let canonical_path = state
2460 .canonicalize(&path, true)
2461 .with_context(|| format!("path does not exist: {path:?}"))?;
2462 Ok(canonical_path)
2463 }
2464
2465 async fn is_file(&self, path: &Path) -> bool {
2466 let path = normalize_path(path);
2467 self.simulate_random_delay().await;
2468 let mut state = self.state.lock();
2469 if let Some((entry, _)) = state.try_entry(&path, true) {
2470 entry.is_file()
2471 } else {
2472 false
2473 }
2474 }
2475
2476 async fn is_dir(&self, path: &Path) -> bool {
2477 self.metadata(path)
2478 .await
2479 .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
2480 }
2481
2482 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
2483 self.simulate_random_delay().await;
2484 let path = normalize_path(path);
2485 let mut state = self.state.lock();
2486 state.metadata_call_count += 1;
2487 if let Some((mut entry, _)) = state.try_entry(&path, false) {
2488 let is_symlink = entry.is_symlink();
2489 if is_symlink {
2490 if let Some(e) = state.try_entry(&path, true).map(|e| e.0) {
2491 entry = e;
2492 } else {
2493 return Ok(None);
2494 }
2495 }
2496
2497 Ok(Some(match &*entry {
2498 FakeFsEntry::File {
2499 inode, mtime, len, ..
2500 } => Metadata {
2501 inode: *inode,
2502 mtime: *mtime,
2503 len: *len,
2504 is_dir: false,
2505 is_symlink,
2506 is_fifo: false,
2507 },
2508 FakeFsEntry::Dir {
2509 inode, mtime, len, ..
2510 } => Metadata {
2511 inode: *inode,
2512 mtime: *mtime,
2513 len: *len,
2514 is_dir: true,
2515 is_symlink,
2516 is_fifo: false,
2517 },
2518 FakeFsEntry::Symlink { .. } => unreachable!(),
2519 }))
2520 } else {
2521 Ok(None)
2522 }
2523 }
2524
2525 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
2526 self.simulate_random_delay().await;
2527 let path = normalize_path(path);
2528 let mut state = self.state.lock();
2529 let (entry, _) = state
2530 .try_entry(&path, false)
2531 .with_context(|| format!("path does not exist: {path:?}"))?;
2532 if let FakeFsEntry::Symlink { target } = entry {
2533 Ok(target.clone())
2534 } else {
2535 anyhow::bail!("not a symlink: {path:?}")
2536 }
2537 }
2538
2539 async fn read_dir(
2540 &self,
2541 path: &Path,
2542 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
2543 self.simulate_random_delay().await;
2544 let path = normalize_path(path);
2545 let mut state = self.state.lock();
2546 state.read_dir_call_count += 1;
2547 let entry = state.entry(&path)?;
2548 let children = entry.dir_entries(&path)?;
2549 let paths = children
2550 .keys()
2551 .map(|file_name| Ok(path.join(file_name)))
2552 .collect::<Vec<_>>();
2553 Ok(Box::pin(futures::stream::iter(paths)))
2554 }
2555
2556 async fn watch(
2557 &self,
2558 path: &Path,
2559 _: Duration,
2560 ) -> (
2561 Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
2562 Arc<dyn Watcher>,
2563 ) {
2564 self.simulate_random_delay().await;
2565 let (tx, rx) = smol::channel::unbounded();
2566 let path = path.to_path_buf();
2567 self.state.lock().event_txs.push((path.clone(), tx.clone()));
2568 let executor = self.executor.clone();
2569 let watcher = Arc::new(FakeWatcher {
2570 tx,
2571 original_path: path.to_owned(),
2572 fs_state: self.state.clone(),
2573 prefixes: Mutex::new(vec![path]),
2574 });
2575 (
2576 Box::pin(futures::StreamExt::filter(rx, {
2577 let watcher = watcher.clone();
2578 move |events| {
2579 let result = events.iter().any(|evt_path| {
2580 watcher
2581 .prefixes
2582 .lock()
2583 .iter()
2584 .any(|prefix| evt_path.path.starts_with(prefix))
2585 });
2586 let executor = executor.clone();
2587 async move {
2588 executor.simulate_random_delay().await;
2589 result
2590 }
2591 }
2592 })),
2593 watcher,
2594 )
2595 }
2596
2597 fn open_repo(
2598 &self,
2599 abs_dot_git: &Path,
2600 _system_git_binary: Option<&Path>,
2601 ) -> Option<Arc<dyn GitRepository>> {
2602 use util::ResultExt as _;
2603
2604 self.with_git_state_and_paths(
2605 abs_dot_git,
2606 false,
2607 |_, repository_dir_path, common_dir_path| {
2608 Arc::new(fake_git_repo::FakeGitRepository {
2609 fs: self.this.upgrade().unwrap(),
2610 executor: self.executor.clone(),
2611 dot_git_path: abs_dot_git.to_path_buf(),
2612 repository_dir_path: repository_dir_path.to_owned(),
2613 common_dir_path: common_dir_path.to_owned(),
2614 checkpoints: Arc::default(),
2615 }) as _
2616 },
2617 )
2618 .log_err()
2619 }
2620
2621 async fn git_init(
2622 &self,
2623 abs_work_directory_path: &Path,
2624 _fallback_branch_name: String,
2625 ) -> Result<()> {
2626 self.create_dir(&abs_work_directory_path.join(".git")).await
2627 }
2628
2629 async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> {
2630 anyhow::bail!("Git clone is not supported in fake Fs")
2631 }
2632
2633 fn is_fake(&self) -> bool {
2634 true
2635 }
2636
2637 async fn is_case_sensitive(&self) -> Result<bool> {
2638 Ok(true)
2639 }
2640
2641 #[cfg(any(test, feature = "test-support"))]
2642 fn as_fake(&self) -> Arc<FakeFs> {
2643 self.this.upgrade().unwrap()
2644 }
2645}
2646
2647fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
2648 rope.chunks().flat_map(move |chunk| {
2649 let mut newline = false;
2650 let end_with_newline = chunk.ends_with('\n').then_some(line_ending.as_str());
2651 chunk
2652 .lines()
2653 .flat_map(move |line| {
2654 let ending = if newline {
2655 Some(line_ending.as_str())
2656 } else {
2657 None
2658 };
2659 newline = true;
2660 ending.into_iter().chain([line])
2661 })
2662 .chain(end_with_newline)
2663 })
2664}
2665
2666pub fn normalize_path(path: &Path) -> PathBuf {
2667 let mut components = path.components().peekable();
2668 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
2669 components.next();
2670 PathBuf::from(c.as_os_str())
2671 } else {
2672 PathBuf::new()
2673 };
2674
2675 for component in components {
2676 match component {
2677 Component::Prefix(..) => unreachable!(),
2678 Component::RootDir => {
2679 ret.push(component.as_os_str());
2680 }
2681 Component::CurDir => {}
2682 Component::ParentDir => {
2683 ret.pop();
2684 }
2685 Component::Normal(c) => {
2686 ret.push(c);
2687 }
2688 }
2689 }
2690 ret
2691}
2692
2693pub async fn copy_recursive<'a>(
2694 fs: &'a dyn Fs,
2695 source: &'a Path,
2696 target: &'a Path,
2697 options: CopyOptions,
2698) -> Result<()> {
2699 for (item, is_dir) in read_dir_items(fs, source).await? {
2700 let Ok(item_relative_path) = item.strip_prefix(source) else {
2701 continue;
2702 };
2703 let target_item = if item_relative_path == Path::new("") {
2704 target.to_path_buf()
2705 } else {
2706 target.join(item_relative_path)
2707 };
2708 if is_dir {
2709 if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
2710 if options.ignore_if_exists {
2711 continue;
2712 } else {
2713 anyhow::bail!("{target_item:?} already exists");
2714 }
2715 }
2716 let _ = fs
2717 .remove_dir(
2718 &target_item,
2719 RemoveOptions {
2720 recursive: true,
2721 ignore_if_not_exists: true,
2722 },
2723 )
2724 .await;
2725 fs.create_dir(&target_item).await?;
2726 } else {
2727 fs.copy_file(&item, &target_item, options).await?;
2728 }
2729 }
2730 Ok(())
2731}
2732
2733/// Recursively reads all of the paths in the given directory.
2734///
2735/// Returns a vector of tuples of (path, is_dir).
2736pub async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(PathBuf, bool)>> {
2737 let mut items = Vec::new();
2738 read_recursive(fs, source, &mut items).await?;
2739 Ok(items)
2740}
2741
2742fn read_recursive<'a>(
2743 fs: &'a dyn Fs,
2744 source: &'a Path,
2745 output: &'a mut Vec<(PathBuf, bool)>,
2746) -> BoxFuture<'a, Result<()>> {
2747 use futures::future::FutureExt;
2748
2749 async move {
2750 let metadata = fs
2751 .metadata(source)
2752 .await?
2753 .with_context(|| format!("path does not exist: {source:?}"))?;
2754
2755 if metadata.is_dir {
2756 output.push((source.to_path_buf(), true));
2757 let mut children = fs.read_dir(source).await?;
2758 while let Some(child_path) = children.next().await {
2759 if let Ok(child_path) = child_path {
2760 read_recursive(fs, &child_path, output).await?;
2761 }
2762 }
2763 } else {
2764 output.push((source.to_path_buf(), false));
2765 }
2766 Ok(())
2767 }
2768 .boxed()
2769}
2770
2771// todo(windows)
2772// can we get file id not open the file twice?
2773// https://github.com/rust-lang/rust/issues/63010
2774#[cfg(target_os = "windows")]
2775async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
2776 use std::os::windows::io::AsRawHandle;
2777
2778 use smol::fs::windows::OpenOptionsExt;
2779 use windows::Win32::{
2780 Foundation::HANDLE,
2781 Storage::FileSystem::{
2782 BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS, GetFileInformationByHandle,
2783 },
2784 };
2785
2786 let file = smol::fs::OpenOptions::new()
2787 .read(true)
2788 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
2789 .open(path)
2790 .await?;
2791
2792 let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
2793 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
2794 // This function supports Windows XP+
2795 smol::unblock(move || {
2796 unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
2797
2798 Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
2799 })
2800 .await
2801}
2802
2803#[cfg(target_os = "windows")]
2804fn atomic_replace<P: AsRef<Path>>(
2805 replaced_file: P,
2806 replacement_file: P,
2807) -> windows::core::Result<()> {
2808 use windows::{
2809 Win32::Storage::FileSystem::{REPLACE_FILE_FLAGS, ReplaceFileW},
2810 core::HSTRING,
2811 };
2812
2813 // If the file does not exist, create it.
2814 let _ = std::fs::File::create_new(replaced_file.as_ref());
2815
2816 unsafe {
2817 ReplaceFileW(
2818 &HSTRING::from(replaced_file.as_ref().to_string_lossy().into_owned()),
2819 &HSTRING::from(replacement_file.as_ref().to_string_lossy().into_owned()),
2820 None,
2821 REPLACE_FILE_FLAGS::default(),
2822 None,
2823 None,
2824 )
2825 }
2826}
2827
2828#[cfg(test)]
2829mod tests {
2830 use super::*;
2831 use gpui::BackgroundExecutor;
2832 use serde_json::json;
2833 use util::path;
2834
2835 #[gpui::test]
2836 async fn test_fake_fs(executor: BackgroundExecutor) {
2837 let fs = FakeFs::new(executor.clone());
2838 fs.insert_tree(
2839 path!("/root"),
2840 json!({
2841 "dir1": {
2842 "a": "A",
2843 "b": "B"
2844 },
2845 "dir2": {
2846 "c": "C",
2847 "dir3": {
2848 "d": "D"
2849 }
2850 }
2851 }),
2852 )
2853 .await;
2854
2855 assert_eq!(
2856 fs.files(),
2857 vec![
2858 PathBuf::from(path!("/root/dir1/a")),
2859 PathBuf::from(path!("/root/dir1/b")),
2860 PathBuf::from(path!("/root/dir2/c")),
2861 PathBuf::from(path!("/root/dir2/dir3/d")),
2862 ]
2863 );
2864
2865 fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
2866 .await
2867 .unwrap();
2868
2869 assert_eq!(
2870 fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
2871 .await
2872 .unwrap(),
2873 PathBuf::from(path!("/root/dir2/dir3")),
2874 );
2875 assert_eq!(
2876 fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
2877 .await
2878 .unwrap(),
2879 PathBuf::from(path!("/root/dir2/dir3/d")),
2880 );
2881 assert_eq!(
2882 fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
2883 .await
2884 .unwrap(),
2885 "D",
2886 );
2887 }
2888
2889 #[gpui::test]
2890 async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
2891 let fs = FakeFs::new(executor.clone());
2892 fs.insert_tree(
2893 path!("/outer"),
2894 json!({
2895 "a": "A",
2896 "b": "B",
2897 "inner": {}
2898 }),
2899 )
2900 .await;
2901
2902 assert_eq!(
2903 fs.files(),
2904 vec![
2905 PathBuf::from(path!("/outer/a")),
2906 PathBuf::from(path!("/outer/b")),
2907 ]
2908 );
2909
2910 let source = Path::new(path!("/outer/a"));
2911 let target = Path::new(path!("/outer/a copy"));
2912 copy_recursive(fs.as_ref(), source, target, Default::default())
2913 .await
2914 .unwrap();
2915
2916 assert_eq!(
2917 fs.files(),
2918 vec![
2919 PathBuf::from(path!("/outer/a")),
2920 PathBuf::from(path!("/outer/a copy")),
2921 PathBuf::from(path!("/outer/b")),
2922 ]
2923 );
2924
2925 let source = Path::new(path!("/outer/a"));
2926 let target = Path::new(path!("/outer/inner/a copy"));
2927 copy_recursive(fs.as_ref(), source, target, Default::default())
2928 .await
2929 .unwrap();
2930
2931 assert_eq!(
2932 fs.files(),
2933 vec![
2934 PathBuf::from(path!("/outer/a")),
2935 PathBuf::from(path!("/outer/a copy")),
2936 PathBuf::from(path!("/outer/b")),
2937 PathBuf::from(path!("/outer/inner/a copy")),
2938 ]
2939 );
2940 }
2941
2942 #[gpui::test]
2943 async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
2944 let fs = FakeFs::new(executor.clone());
2945 fs.insert_tree(
2946 path!("/outer"),
2947 json!({
2948 "a": "A",
2949 "empty": {},
2950 "non-empty": {
2951 "b": "B",
2952 }
2953 }),
2954 )
2955 .await;
2956
2957 assert_eq!(
2958 fs.files(),
2959 vec![
2960 PathBuf::from(path!("/outer/a")),
2961 PathBuf::from(path!("/outer/non-empty/b")),
2962 ]
2963 );
2964 assert_eq!(
2965 fs.directories(false),
2966 vec![
2967 PathBuf::from(path!("/")),
2968 PathBuf::from(path!("/outer")),
2969 PathBuf::from(path!("/outer/empty")),
2970 PathBuf::from(path!("/outer/non-empty")),
2971 ]
2972 );
2973
2974 let source = Path::new(path!("/outer/empty"));
2975 let target = Path::new(path!("/outer/empty copy"));
2976 copy_recursive(fs.as_ref(), source, target, Default::default())
2977 .await
2978 .unwrap();
2979
2980 assert_eq!(
2981 fs.files(),
2982 vec![
2983 PathBuf::from(path!("/outer/a")),
2984 PathBuf::from(path!("/outer/non-empty/b")),
2985 ]
2986 );
2987 assert_eq!(
2988 fs.directories(false),
2989 vec![
2990 PathBuf::from(path!("/")),
2991 PathBuf::from(path!("/outer")),
2992 PathBuf::from(path!("/outer/empty")),
2993 PathBuf::from(path!("/outer/empty copy")),
2994 PathBuf::from(path!("/outer/non-empty")),
2995 ]
2996 );
2997
2998 let source = Path::new(path!("/outer/non-empty"));
2999 let target = Path::new(path!("/outer/non-empty copy"));
3000 copy_recursive(fs.as_ref(), source, target, Default::default())
3001 .await
3002 .unwrap();
3003
3004 assert_eq!(
3005 fs.files(),
3006 vec![
3007 PathBuf::from(path!("/outer/a")),
3008 PathBuf::from(path!("/outer/non-empty/b")),
3009 PathBuf::from(path!("/outer/non-empty copy/b")),
3010 ]
3011 );
3012 assert_eq!(
3013 fs.directories(false),
3014 vec![
3015 PathBuf::from(path!("/")),
3016 PathBuf::from(path!("/outer")),
3017 PathBuf::from(path!("/outer/empty")),
3018 PathBuf::from(path!("/outer/empty copy")),
3019 PathBuf::from(path!("/outer/non-empty")),
3020 PathBuf::from(path!("/outer/non-empty copy")),
3021 ]
3022 );
3023 }
3024
3025 #[gpui::test]
3026 async fn test_copy_recursive(executor: BackgroundExecutor) {
3027 let fs = FakeFs::new(executor.clone());
3028 fs.insert_tree(
3029 path!("/outer"),
3030 json!({
3031 "inner1": {
3032 "a": "A",
3033 "b": "B",
3034 "inner3": {
3035 "d": "D",
3036 },
3037 "inner4": {}
3038 },
3039 "inner2": {
3040 "c": "C",
3041 }
3042 }),
3043 )
3044 .await;
3045
3046 assert_eq!(
3047 fs.files(),
3048 vec![
3049 PathBuf::from(path!("/outer/inner1/a")),
3050 PathBuf::from(path!("/outer/inner1/b")),
3051 PathBuf::from(path!("/outer/inner2/c")),
3052 PathBuf::from(path!("/outer/inner1/inner3/d")),
3053 ]
3054 );
3055 assert_eq!(
3056 fs.directories(false),
3057 vec![
3058 PathBuf::from(path!("/")),
3059 PathBuf::from(path!("/outer")),
3060 PathBuf::from(path!("/outer/inner1")),
3061 PathBuf::from(path!("/outer/inner2")),
3062 PathBuf::from(path!("/outer/inner1/inner3")),
3063 PathBuf::from(path!("/outer/inner1/inner4")),
3064 ]
3065 );
3066
3067 let source = Path::new(path!("/outer"));
3068 let target = Path::new(path!("/outer/inner1/outer"));
3069 copy_recursive(fs.as_ref(), source, target, Default::default())
3070 .await
3071 .unwrap();
3072
3073 assert_eq!(
3074 fs.files(),
3075 vec![
3076 PathBuf::from(path!("/outer/inner1/a")),
3077 PathBuf::from(path!("/outer/inner1/b")),
3078 PathBuf::from(path!("/outer/inner2/c")),
3079 PathBuf::from(path!("/outer/inner1/inner3/d")),
3080 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3081 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3082 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3083 PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
3084 ]
3085 );
3086 assert_eq!(
3087 fs.directories(false),
3088 vec![
3089 PathBuf::from(path!("/")),
3090 PathBuf::from(path!("/outer")),
3091 PathBuf::from(path!("/outer/inner1")),
3092 PathBuf::from(path!("/outer/inner2")),
3093 PathBuf::from(path!("/outer/inner1/inner3")),
3094 PathBuf::from(path!("/outer/inner1/inner4")),
3095 PathBuf::from(path!("/outer/inner1/outer")),
3096 PathBuf::from(path!("/outer/inner1/outer/inner1")),
3097 PathBuf::from(path!("/outer/inner1/outer/inner2")),
3098 PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
3099 PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
3100 ]
3101 );
3102 }
3103
3104 #[gpui::test]
3105 async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
3106 let fs = FakeFs::new(executor.clone());
3107 fs.insert_tree(
3108 path!("/outer"),
3109 json!({
3110 "inner1": {
3111 "a": "A",
3112 "b": "B",
3113 "outer": {
3114 "inner1": {
3115 "a": "B"
3116 }
3117 }
3118 },
3119 "inner2": {
3120 "c": "C",
3121 }
3122 }),
3123 )
3124 .await;
3125
3126 assert_eq!(
3127 fs.files(),
3128 vec![
3129 PathBuf::from(path!("/outer/inner1/a")),
3130 PathBuf::from(path!("/outer/inner1/b")),
3131 PathBuf::from(path!("/outer/inner2/c")),
3132 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3133 ]
3134 );
3135 assert_eq!(
3136 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3137 .await
3138 .unwrap(),
3139 "B",
3140 );
3141
3142 let source = Path::new(path!("/outer"));
3143 let target = Path::new(path!("/outer/inner1/outer"));
3144 copy_recursive(
3145 fs.as_ref(),
3146 source,
3147 target,
3148 CopyOptions {
3149 overwrite: true,
3150 ..Default::default()
3151 },
3152 )
3153 .await
3154 .unwrap();
3155
3156 assert_eq!(
3157 fs.files(),
3158 vec![
3159 PathBuf::from(path!("/outer/inner1/a")),
3160 PathBuf::from(path!("/outer/inner1/b")),
3161 PathBuf::from(path!("/outer/inner2/c")),
3162 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3163 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3164 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3165 PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
3166 ]
3167 );
3168 assert_eq!(
3169 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3170 .await
3171 .unwrap(),
3172 "A"
3173 );
3174 }
3175
3176 #[gpui::test]
3177 async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
3178 let fs = FakeFs::new(executor.clone());
3179 fs.insert_tree(
3180 path!("/outer"),
3181 json!({
3182 "inner1": {
3183 "a": "A",
3184 "b": "B",
3185 "outer": {
3186 "inner1": {
3187 "a": "B"
3188 }
3189 }
3190 },
3191 "inner2": {
3192 "c": "C",
3193 }
3194 }),
3195 )
3196 .await;
3197
3198 assert_eq!(
3199 fs.files(),
3200 vec![
3201 PathBuf::from(path!("/outer/inner1/a")),
3202 PathBuf::from(path!("/outer/inner1/b")),
3203 PathBuf::from(path!("/outer/inner2/c")),
3204 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3205 ]
3206 );
3207 assert_eq!(
3208 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3209 .await
3210 .unwrap(),
3211 "B",
3212 );
3213
3214 let source = Path::new(path!("/outer"));
3215 let target = Path::new(path!("/outer/inner1/outer"));
3216 copy_recursive(
3217 fs.as_ref(),
3218 source,
3219 target,
3220 CopyOptions {
3221 ignore_if_exists: true,
3222 ..Default::default()
3223 },
3224 )
3225 .await
3226 .unwrap();
3227
3228 assert_eq!(
3229 fs.files(),
3230 vec![
3231 PathBuf::from(path!("/outer/inner1/a")),
3232 PathBuf::from(path!("/outer/inner1/b")),
3233 PathBuf::from(path!("/outer/inner2/c")),
3234 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3235 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3236 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3237 PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
3238 ]
3239 );
3240 assert_eq!(
3241 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3242 .await
3243 .unwrap(),
3244 "B"
3245 );
3246 }
3247
3248 #[gpui::test]
3249 async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
3250 // With the file handle still open, the file should be replaced
3251 // https://github.com/zed-industries/zed/issues/30054
3252 let fs = RealFs {
3253 bundled_git_binary_path: None,
3254 executor,
3255 };
3256 let temp_dir = TempDir::new().unwrap();
3257 let file_to_be_replaced = temp_dir.path().join("file.txt");
3258 let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
3259 file.write_all(b"Hello").unwrap();
3260 // drop(file); // We still hold the file handle here
3261 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3262 assert_eq!(content, "Hello");
3263 smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
3264 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3265 assert_eq!(content, "World");
3266 }
3267
3268 #[gpui::test]
3269 async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
3270 let fs = RealFs {
3271 bundled_git_binary_path: None,
3272 executor,
3273 };
3274 let temp_dir = TempDir::new().unwrap();
3275 let file_to_be_replaced = temp_dir.path().join("file.txt");
3276 smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
3277 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3278 assert_eq!(content, "Hello");
3279 }
3280}