1use anyhow::{anyhow, Result};
2use git::GitHostingProviderRegistry;
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6
7use async_tar::Archive;
8use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
9use git::repository::{GitRepository, RealGitRepository};
10use git2::Repository as LibGitRepository;
11use parking_lot::Mutex;
12use rope::Rope;
13#[cfg(any(test, feature = "test-support"))]
14use smol::io::AsyncReadExt;
15use smol::io::AsyncWriteExt;
16use std::io::Write;
17use std::sync::Arc;
18use std::{
19 io,
20 path::{Component, Path, PathBuf},
21 pin::Pin,
22 time::{Duration, SystemTime},
23};
24use tempfile::{NamedTempFile, TempDir};
25use text::LineEnding;
26use util::{paths, ResultExt};
27
28#[cfg(any(test, feature = "test-support"))]
29use collections::{btree_map, BTreeMap};
30#[cfg(any(test, feature = "test-support"))]
31use git::repository::{FakeGitRepositoryState, GitFileStatus};
32#[cfg(any(test, feature = "test-support"))]
33use std::ffi::OsStr;
34
35#[async_trait::async_trait]
36pub trait Fs: Send + Sync {
37 async fn create_dir(&self, path: &Path) -> Result<()>;
38 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>;
39 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
40 async fn create_file_with(
41 &self,
42 path: &Path,
43 content: Pin<&mut (dyn AsyncRead + Send)>,
44 ) -> Result<()>;
45 async fn extract_tar_file(
46 &self,
47 path: &Path,
48 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
49 ) -> Result<()>;
50 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
51 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
52 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
53 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
54 self.remove_dir(path, options).await
55 }
56 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
57 async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
58 self.remove_file(path, options).await
59 }
60 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
61 async fn load(&self, path: &Path) -> Result<String>;
62 async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
63 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
64 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
65 async fn is_file(&self, path: &Path) -> bool;
66 async fn is_dir(&self, path: &Path) -> bool;
67 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
68 async fn read_link(&self, path: &Path) -> Result<PathBuf>;
69 async fn read_dir(
70 &self,
71 path: &Path,
72 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
73
74 async fn watch(
75 &self,
76 path: &Path,
77 latency: Duration,
78 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
79
80 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
81 fn is_fake(&self) -> bool;
82 async fn is_case_sensitive(&self) -> Result<bool>;
83 #[cfg(any(test, feature = "test-support"))]
84 fn as_fake(&self) -> &FakeFs;
85}
86
87#[derive(Copy, Clone, Default)]
88pub struct CreateOptions {
89 pub overwrite: bool,
90 pub ignore_if_exists: bool,
91}
92
93#[derive(Copy, Clone, Default)]
94pub struct CopyOptions {
95 pub overwrite: bool,
96 pub ignore_if_exists: bool,
97}
98
99#[derive(Copy, Clone, Default)]
100pub struct RenameOptions {
101 pub overwrite: bool,
102 pub ignore_if_exists: bool,
103}
104
105#[derive(Copy, Clone, Default)]
106pub struct RemoveOptions {
107 pub recursive: bool,
108 pub ignore_if_not_exists: bool,
109}
110
111#[derive(Copy, Clone, Debug)]
112pub struct Metadata {
113 pub inode: u64,
114 pub mtime: SystemTime,
115 pub is_symlink: bool,
116 pub is_dir: bool,
117}
118
119#[derive(Default)]
120pub struct RealFs {
121 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
122 git_binary_path: Option<PathBuf>,
123}
124
125impl RealFs {
126 pub fn new(
127 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
128 git_binary_path: Option<PathBuf>,
129 ) -> Self {
130 Self {
131 git_hosting_provider_registry,
132 git_binary_path,
133 }
134 }
135}
136
137#[async_trait::async_trait]
138impl Fs for RealFs {
139 async fn create_dir(&self, path: &Path) -> Result<()> {
140 Ok(smol::fs::create_dir_all(path).await?)
141 }
142
143 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
144 #[cfg(unix)]
145 smol::fs::unix::symlink(target, path).await?;
146
147 #[cfg(windows)]
148 if smol::fs::metadata(&target).await?.is_dir() {
149 smol::fs::windows::symlink_dir(target, path).await?
150 } else {
151 smol::fs::windows::symlink_file(target, path).await?
152 }
153
154 Ok(())
155 }
156
157 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
158 let mut open_options = smol::fs::OpenOptions::new();
159 open_options.write(true).create(true);
160 if options.overwrite {
161 open_options.truncate(true);
162 } else if !options.ignore_if_exists {
163 open_options.create_new(true);
164 }
165 open_options.open(path).await?;
166 Ok(())
167 }
168
169 async fn create_file_with(
170 &self,
171 path: &Path,
172 content: Pin<&mut (dyn AsyncRead + Send)>,
173 ) -> Result<()> {
174 let mut file = smol::fs::File::create(&path).await?;
175 futures::io::copy(content, &mut file).await?;
176 Ok(())
177 }
178
179 async fn extract_tar_file(
180 &self,
181 path: &Path,
182 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
183 ) -> Result<()> {
184 content.unpack(path).await?;
185 Ok(())
186 }
187
188 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
189 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
190 if options.ignore_if_exists {
191 return Ok(());
192 } else {
193 return Err(anyhow!("{target:?} already exists"));
194 }
195 }
196
197 smol::fs::copy(source, target).await?;
198 Ok(())
199 }
200
201 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
202 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
203 if options.ignore_if_exists {
204 return Ok(());
205 } else {
206 return Err(anyhow!("{target:?} already exists"));
207 }
208 }
209
210 smol::fs::rename(source, target).await?;
211 Ok(())
212 }
213
214 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
215 let result = if options.recursive {
216 smol::fs::remove_dir_all(path).await
217 } else {
218 smol::fs::remove_dir(path).await
219 };
220 match result {
221 Ok(()) => Ok(()),
222 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
223 Ok(())
224 }
225 Err(err) => Err(err)?,
226 }
227 }
228
229 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
230 #[cfg(windows)]
231 if let Ok(Some(metadata)) = self.metadata(path).await {
232 if metadata.is_symlink && metadata.is_dir {
233 self.remove_dir(
234 path,
235 RemoveOptions {
236 recursive: false,
237 ignore_if_not_exists: true,
238 },
239 )
240 .await?;
241 return Ok(());
242 }
243 }
244
245 match smol::fs::remove_file(path).await {
246 Ok(()) => Ok(()),
247 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
248 Ok(())
249 }
250 Err(err) => Err(err)?,
251 }
252 }
253
254 #[cfg(target_os = "macos")]
255 async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
256 use cocoa::{
257 base::{id, nil},
258 foundation::{NSAutoreleasePool, NSString},
259 };
260 use objc::{class, msg_send, sel, sel_impl};
261
262 unsafe {
263 unsafe fn ns_string(string: &str) -> id {
264 NSString::alloc(nil).init_str(string).autorelease()
265 }
266
267 let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
268 let array: id = msg_send![class!(NSArray), arrayWithObject: url];
269 let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
270
271 let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
272 }
273 Ok(())
274 }
275
276 #[cfg(target_os = "macos")]
277 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
278 self.trash_file(path, options).await
279 }
280
281 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
282 Ok(Box::new(std::fs::File::open(path)?))
283 }
284
285 async fn load(&self, path: &Path) -> Result<String> {
286 let path = path.to_path_buf();
287 let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
288 Ok(text)
289 }
290
291 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
292 smol::unblock(move || {
293 let mut tmp_file = if cfg!(target_os = "linux") {
294 // Use the directory of the destination as temp dir to avoid
295 // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
296 // See https://github.com/zed-industries/zed/pull/8437 for more details.
297 NamedTempFile::new_in(path.parent().unwrap_or(&paths::TEMP_DIR))
298 } else {
299 NamedTempFile::new()
300 }?;
301 tmp_file.write_all(data.as_bytes())?;
302 tmp_file.persist(path)?;
303 Ok::<(), anyhow::Error>(())
304 })
305 .await?;
306
307 Ok(())
308 }
309
310 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
311 let buffer_size = text.summary().len.min(10 * 1024);
312 if let Some(path) = path.parent() {
313 self.create_dir(path).await?;
314 }
315 let file = smol::fs::File::create(path).await?;
316 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
317 for chunk in chunks(text, line_ending) {
318 writer.write_all(chunk.as_bytes()).await?;
319 }
320 writer.flush().await?;
321 Ok(())
322 }
323
324 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
325 Ok(smol::fs::canonicalize(path).await?)
326 }
327
328 async fn is_file(&self, path: &Path) -> bool {
329 smol::fs::metadata(path)
330 .await
331 .map_or(false, |metadata| metadata.is_file())
332 }
333
334 async fn is_dir(&self, path: &Path) -> bool {
335 smol::fs::metadata(path)
336 .await
337 .map_or(false, |metadata| metadata.is_dir())
338 }
339
340 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
341 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
342 Ok(metadata) => metadata,
343 Err(err) => {
344 return match (err.kind(), err.raw_os_error()) {
345 (io::ErrorKind::NotFound, _) => Ok(None),
346 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
347 _ => Err(anyhow::Error::new(err)),
348 }
349 }
350 };
351
352 let is_symlink = symlink_metadata.file_type().is_symlink();
353 let metadata = if is_symlink {
354 smol::fs::metadata(path).await?
355 } else {
356 symlink_metadata
357 };
358
359 #[cfg(unix)]
360 let inode = metadata.ino();
361
362 #[cfg(windows)]
363 let inode = file_id(path).await?;
364
365 Ok(Some(Metadata {
366 inode,
367 mtime: metadata.modified().unwrap(),
368 is_symlink,
369 is_dir: metadata.file_type().is_dir(),
370 }))
371 }
372
373 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
374 let path = smol::fs::read_link(path).await?;
375 Ok(path)
376 }
377
378 async fn read_dir(
379 &self,
380 path: &Path,
381 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
382 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
383 Ok(entry) => Ok(entry.path()),
384 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
385 });
386 Ok(Box::pin(result))
387 }
388
389 #[cfg(target_os = "macos")]
390 async fn watch(
391 &self,
392 path: &Path,
393 latency: Duration,
394 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
395 use fsevent::EventStream;
396
397 let (tx, rx) = smol::channel::unbounded();
398 let (stream, handle) = EventStream::new(&[path], latency);
399 std::thread::spawn(move || {
400 stream.run(move |events| {
401 smol::block_on(tx.send(events.into_iter().map(|event| event.path).collect()))
402 .is_ok()
403 });
404 });
405
406 Box::pin(rx.chain(futures::stream::once(async move {
407 drop(handle);
408 vec![]
409 })))
410 }
411
412 #[cfg(not(target_os = "macos"))]
413 async fn watch(
414 &self,
415 path: &Path,
416 _latency: Duration,
417 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
418 use notify::{event::EventKind, event::ModifyKind, Watcher};
419 // todo(linux): This spawns two threads, while the macOS impl
420 // only spawns one. Can we use a OnceLock or some such to make
421 // this better
422
423 let (tx, rx) = smol::channel::unbounded();
424
425 let mut file_watcher = notify::recommended_watcher({
426 let tx = tx.clone();
427 move |event: Result<notify::Event, _>| {
428 if let Some(event) = event.log_err() {
429 tx.try_send(event.paths).ok();
430 }
431 }
432 })
433 .expect("Could not start file watcher");
434
435 file_watcher
436 .watch(path, notify::RecursiveMode::Recursive)
437 .ok(); // It's ok if this fails, the parent watcher will add it.
438
439 let mut parent_watcher = notify::recommended_watcher({
440 let watched_path = path.to_path_buf();
441 let tx = tx.clone();
442 move |event: Result<notify::Event, _>| {
443 if let Some(event) = event.ok() {
444 if event.paths.into_iter().any(|path| *path == watched_path) {
445 match event.kind {
446 EventKind::Modify(ev) => {
447 if matches!(ev, ModifyKind::Name(_)) {
448 file_watcher
449 .watch(
450 watched_path.as_path(),
451 notify::RecursiveMode::Recursive,
452 )
453 .log_err();
454 let _ = tx.try_send(vec![watched_path.clone()]).ok();
455 }
456 }
457 EventKind::Create(_) => {
458 file_watcher
459 .watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
460 .log_err();
461 let _ = tx.try_send(vec![watched_path.clone()]).ok();
462 }
463 EventKind::Remove(_) => {
464 file_watcher.unwatch(&watched_path).log_err();
465 let _ = tx.try_send(vec![watched_path.clone()]).ok();
466 }
467 _ => {}
468 }
469 }
470 }
471 }
472 })
473 .expect("Could not start file watcher");
474
475 parent_watcher
476 .watch(
477 path.parent()
478 .expect("Watching root is probably not what you want"),
479 notify::RecursiveMode::NonRecursive,
480 )
481 .expect("Could not start watcher on parent directory");
482
483 Box::pin(rx.chain(futures::stream::once(async move {
484 drop(parent_watcher);
485 vec![]
486 })))
487 }
488
489 fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
490 LibGitRepository::open(dotgit_path)
491 .log_err()
492 .map::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
493 Arc::new(Mutex::new(RealGitRepository::new(
494 libgit_repository,
495 self.git_binary_path.clone(),
496 self.git_hosting_provider_registry.clone(),
497 )))
498 })
499 }
500
501 fn is_fake(&self) -> bool {
502 false
503 }
504
505 /// Checks whether the file system is case sensitive by attempting to create two files
506 /// that have the same name except for the casing.
507 ///
508 /// It creates both files in a temporary directory it removes at the end.
509 async fn is_case_sensitive(&self) -> Result<bool> {
510 let temp_dir = TempDir::new()?;
511 let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
512 let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
513
514 let create_opts = CreateOptions {
515 overwrite: false,
516 ignore_if_exists: false,
517 };
518
519 // Create file1
520 self.create_file(&test_file_1, create_opts).await?;
521
522 // Now check whether it's possible to create file2
523 let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
524 Ok(_) => Ok(true),
525 Err(e) => {
526 if let Some(io_error) = e.downcast_ref::<io::Error>() {
527 if io_error.kind() == io::ErrorKind::AlreadyExists {
528 Ok(false)
529 } else {
530 Err(e)
531 }
532 } else {
533 Err(e)
534 }
535 }
536 };
537
538 temp_dir.close()?;
539 case_sensitive
540 }
541
542 #[cfg(any(test, feature = "test-support"))]
543 fn as_fake(&self) -> &FakeFs {
544 panic!("called `RealFs::as_fake`")
545 }
546}
547
548#[cfg(any(test, feature = "test-support"))]
549pub struct FakeFs {
550 // Use an unfair lock to ensure tests are deterministic.
551 state: Mutex<FakeFsState>,
552 executor: gpui::BackgroundExecutor,
553}
554
555#[cfg(any(test, feature = "test-support"))]
556struct FakeFsState {
557 root: Arc<Mutex<FakeFsEntry>>,
558 next_inode: u64,
559 next_mtime: SystemTime,
560 event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
561 events_paused: bool,
562 buffered_events: Vec<PathBuf>,
563 metadata_call_count: usize,
564 read_dir_call_count: usize,
565}
566
567#[cfg(any(test, feature = "test-support"))]
568#[derive(Debug)]
569enum FakeFsEntry {
570 File {
571 inode: u64,
572 mtime: SystemTime,
573 content: Vec<u8>,
574 },
575 Dir {
576 inode: u64,
577 mtime: SystemTime,
578 entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
579 git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
580 },
581 Symlink {
582 target: PathBuf,
583 },
584}
585
586#[cfg(any(test, feature = "test-support"))]
587impl FakeFsState {
588 fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
589 Ok(self
590 .try_read_path(target, true)
591 .ok_or_else(|| {
592 anyhow!(io::Error::new(
593 io::ErrorKind::NotFound,
594 format!("not found: {}", target.display())
595 ))
596 })?
597 .0)
598 }
599
600 fn try_read_path(
601 &self,
602 target: &Path,
603 follow_symlink: bool,
604 ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
605 let mut path = target.to_path_buf();
606 let mut canonical_path = PathBuf::new();
607 let mut entry_stack = Vec::new();
608 'outer: loop {
609 let mut path_components = path.components().peekable();
610 while let Some(component) = path_components.next() {
611 match component {
612 Component::Prefix(_) => panic!("prefix paths aren't supported"),
613 Component::RootDir => {
614 entry_stack.clear();
615 entry_stack.push(self.root.clone());
616 canonical_path.clear();
617 canonical_path.push("/");
618 }
619 Component::CurDir => {}
620 Component::ParentDir => {
621 entry_stack.pop()?;
622 canonical_path.pop();
623 }
624 Component::Normal(name) => {
625 let current_entry = entry_stack.last().cloned()?;
626 let current_entry = current_entry.lock();
627 if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
628 let entry = entries.get(name.to_str().unwrap()).cloned()?;
629 if path_components.peek().is_some() || follow_symlink {
630 let entry = entry.lock();
631 if let FakeFsEntry::Symlink { target, .. } = &*entry {
632 let mut target = target.clone();
633 target.extend(path_components);
634 path = target;
635 continue 'outer;
636 }
637 }
638 entry_stack.push(entry.clone());
639 canonical_path.push(name);
640 } else {
641 return None;
642 }
643 }
644 }
645 }
646 break;
647 }
648 Some((entry_stack.pop()?, canonical_path))
649 }
650
651 fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
652 where
653 Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
654 {
655 let path = normalize_path(path);
656 let filename = path
657 .file_name()
658 .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
659 let parent_path = path.parent().unwrap();
660
661 let parent = self.read_path(parent_path)?;
662 let mut parent = parent.lock();
663 let new_entry = parent
664 .dir_entries(parent_path)?
665 .entry(filename.to_str().unwrap().into());
666 callback(new_entry)
667 }
668
669 fn emit_event<I, T>(&mut self, paths: I)
670 where
671 I: IntoIterator<Item = T>,
672 T: Into<PathBuf>,
673 {
674 self.buffered_events
675 .extend(paths.into_iter().map(Into::into));
676
677 if !self.events_paused {
678 self.flush_events(self.buffered_events.len());
679 }
680 }
681
682 fn flush_events(&mut self, mut count: usize) {
683 count = count.min(self.buffered_events.len());
684 let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
685 self.event_txs.retain(|tx| {
686 let _ = tx.try_send(events.clone());
687 !tx.is_closed()
688 });
689 }
690}
691
692#[cfg(any(test, feature = "test-support"))]
693lazy_static::lazy_static! {
694 pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
695}
696
697#[cfg(any(test, feature = "test-support"))]
698impl FakeFs {
699 pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
700 Arc::new(Self {
701 executor,
702 state: Mutex::new(FakeFsState {
703 root: Arc::new(Mutex::new(FakeFsEntry::Dir {
704 inode: 0,
705 mtime: SystemTime::UNIX_EPOCH,
706 entries: Default::default(),
707 git_repo_state: None,
708 })),
709 next_mtime: SystemTime::UNIX_EPOCH,
710 next_inode: 1,
711 event_txs: Default::default(),
712 buffered_events: Vec::new(),
713 events_paused: false,
714 read_dir_call_count: 0,
715 metadata_call_count: 0,
716 }),
717 })
718 }
719
720 pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
721 self.write_file_internal(path, content).unwrap()
722 }
723
724 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
725 let mut state = self.state.lock();
726 let path = path.as_ref();
727 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
728 state
729 .write_path(path.as_ref(), move |e| match e {
730 btree_map::Entry::Vacant(e) => {
731 e.insert(file);
732 Ok(())
733 }
734 btree_map::Entry::Occupied(mut e) => {
735 *e.get_mut() = file;
736 Ok(())
737 }
738 })
739 .unwrap();
740 state.emit_event([path]);
741 }
742
743 fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
744 let mut state = self.state.lock();
745 let path = path.as_ref();
746 let inode = state.next_inode;
747 let mtime = state.next_mtime;
748 state.next_inode += 1;
749 state.next_mtime += Duration::from_nanos(1);
750 let file = Arc::new(Mutex::new(FakeFsEntry::File {
751 inode,
752 mtime,
753 content,
754 }));
755 state.write_path(path, move |entry| {
756 match entry {
757 btree_map::Entry::Vacant(e) => {
758 e.insert(file);
759 }
760 btree_map::Entry::Occupied(mut e) => {
761 *e.get_mut() = file;
762 }
763 }
764 Ok(())
765 })?;
766 state.emit_event([path]);
767 Ok(())
768 }
769
770 pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
771 let path = path.as_ref();
772 let path = normalize_path(path);
773 let state = self.state.lock();
774 let entry = state.read_path(&path)?;
775 let entry = entry.lock();
776 entry.file_content(&path).cloned()
777 }
778
779 async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
780 let path = path.as_ref();
781 let path = normalize_path(path);
782 self.simulate_random_delay().await;
783 let state = self.state.lock();
784 let entry = state.read_path(&path)?;
785 let entry = entry.lock();
786 entry.file_content(&path).cloned()
787 }
788
789 pub fn pause_events(&self) {
790 self.state.lock().events_paused = true;
791 }
792
793 pub fn buffered_event_count(&self) -> usize {
794 self.state.lock().buffered_events.len()
795 }
796
797 pub fn flush_events(&self, count: usize) {
798 self.state.lock().flush_events(count);
799 }
800
801 #[must_use]
802 pub fn insert_tree<'a>(
803 &'a self,
804 path: impl 'a + AsRef<Path> + Send,
805 tree: serde_json::Value,
806 ) -> futures::future::BoxFuture<'a, ()> {
807 use futures::FutureExt as _;
808 use serde_json::Value::*;
809
810 async move {
811 let path = path.as_ref();
812
813 match tree {
814 Object(map) => {
815 self.create_dir(path).await.unwrap();
816 for (name, contents) in map {
817 let mut path = PathBuf::from(path);
818 path.push(name);
819 self.insert_tree(&path, contents).await;
820 }
821 }
822 Null => {
823 self.create_dir(path).await.unwrap();
824 }
825 String(contents) => {
826 self.insert_file(&path, contents.into_bytes()).await;
827 }
828 _ => {
829 panic!("JSON object must contain only objects, strings, or null");
830 }
831 }
832 }
833 .boxed()
834 }
835
836 pub fn insert_tree_from_real_fs<'a>(
837 &'a self,
838 path: impl 'a + AsRef<Path> + Send,
839 src_path: impl 'a + AsRef<Path> + Send,
840 ) -> futures::future::BoxFuture<'a, ()> {
841 use futures::FutureExt as _;
842
843 async move {
844 let path = path.as_ref();
845 if std::fs::metadata(&src_path).unwrap().is_file() {
846 let contents = std::fs::read(src_path).unwrap();
847 self.insert_file(path, contents).await;
848 } else {
849 self.create_dir(path).await.unwrap();
850 for entry in std::fs::read_dir(&src_path).unwrap() {
851 let entry = entry.unwrap();
852 self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path())
853 .await;
854 }
855 }
856 }
857 .boxed()
858 }
859
860 pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
861 where
862 F: FnOnce(&mut FakeGitRepositoryState),
863 {
864 let mut state = self.state.lock();
865 let entry = state.read_path(dot_git).unwrap();
866 let mut entry = entry.lock();
867
868 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
869 let repo_state = git_repo_state.get_or_insert_with(Default::default);
870 let mut repo_state = repo_state.lock();
871
872 f(&mut repo_state);
873
874 if emit_git_event {
875 state.emit_event([dot_git]);
876 }
877 } else {
878 panic!("not a directory");
879 }
880 }
881
882 pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
883 self.with_git_state(dot_git, true, |state| {
884 state.branch_name = branch.map(Into::into)
885 })
886 }
887
888 pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
889 self.with_git_state(dot_git, true, |state| {
890 state.index_contents.clear();
891 state.index_contents.extend(
892 head_state
893 .iter()
894 .map(|(path, content)| (path.to_path_buf(), content.clone())),
895 );
896 });
897 }
898
899 pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
900 self.with_git_state(dot_git, true, |state| {
901 state.blames.clear();
902 state.blames.extend(
903 blames
904 .into_iter()
905 .map(|(path, blame)| (path.to_path_buf(), blame)),
906 );
907 });
908 }
909
910 pub fn set_status_for_repo_via_working_copy_change(
911 &self,
912 dot_git: &Path,
913 statuses: &[(&Path, GitFileStatus)],
914 ) {
915 self.with_git_state(dot_git, false, |state| {
916 state.worktree_statuses.clear();
917 state.worktree_statuses.extend(
918 statuses
919 .iter()
920 .map(|(path, content)| ((**path).into(), *content)),
921 );
922 });
923 self.state.lock().emit_event(
924 statuses
925 .iter()
926 .map(|(path, _)| dot_git.parent().unwrap().join(path)),
927 );
928 }
929
930 pub fn set_status_for_repo_via_git_operation(
931 &self,
932 dot_git: &Path,
933 statuses: &[(&Path, GitFileStatus)],
934 ) {
935 self.with_git_state(dot_git, true, |state| {
936 state.worktree_statuses.clear();
937 state.worktree_statuses.extend(
938 statuses
939 .iter()
940 .map(|(path, content)| ((**path).into(), *content)),
941 );
942 });
943 }
944
945 pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
946 let mut result = Vec::new();
947 let mut queue = collections::VecDeque::new();
948 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
949 while let Some((path, entry)) = queue.pop_front() {
950 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
951 for (name, entry) in entries {
952 queue.push_back((path.join(name), entry.clone()));
953 }
954 }
955 if include_dot_git
956 || !path
957 .components()
958 .any(|component| component.as_os_str() == *FS_DOT_GIT)
959 {
960 result.push(path);
961 }
962 }
963 result
964 }
965
966 pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
967 let mut result = Vec::new();
968 let mut queue = collections::VecDeque::new();
969 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
970 while let Some((path, entry)) = queue.pop_front() {
971 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
972 for (name, entry) in entries {
973 queue.push_back((path.join(name), entry.clone()));
974 }
975 if include_dot_git
976 || !path
977 .components()
978 .any(|component| component.as_os_str() == *FS_DOT_GIT)
979 {
980 result.push(path);
981 }
982 }
983 }
984 result
985 }
986
987 pub fn files(&self) -> Vec<PathBuf> {
988 let mut result = Vec::new();
989 let mut queue = collections::VecDeque::new();
990 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
991 while let Some((path, entry)) = queue.pop_front() {
992 let e = entry.lock();
993 match &*e {
994 FakeFsEntry::File { .. } => result.push(path),
995 FakeFsEntry::Dir { entries, .. } => {
996 for (name, entry) in entries {
997 queue.push_back((path.join(name), entry.clone()));
998 }
999 }
1000 FakeFsEntry::Symlink { .. } => {}
1001 }
1002 }
1003 result
1004 }
1005
1006 /// How many `read_dir` calls have been issued.
1007 pub fn read_dir_call_count(&self) -> usize {
1008 self.state.lock().read_dir_call_count
1009 }
1010
1011 /// How many `metadata` calls have been issued.
1012 pub fn metadata_call_count(&self) -> usize {
1013 self.state.lock().metadata_call_count
1014 }
1015
1016 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
1017 self.executor.simulate_random_delay()
1018 }
1019}
1020
1021#[cfg(any(test, feature = "test-support"))]
1022impl FakeFsEntry {
1023 fn is_file(&self) -> bool {
1024 matches!(self, Self::File { .. })
1025 }
1026
1027 fn is_symlink(&self) -> bool {
1028 matches!(self, Self::Symlink { .. })
1029 }
1030
1031 fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
1032 if let Self::File { content, .. } = self {
1033 Ok(content)
1034 } else {
1035 Err(anyhow!("not a file: {}", path.display()))
1036 }
1037 }
1038
1039 fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
1040 if let Self::File { content, mtime, .. } = self {
1041 *mtime = SystemTime::now();
1042 *content = new_content;
1043 Ok(())
1044 } else {
1045 Err(anyhow!("not a file: {}", path.display()))
1046 }
1047 }
1048
1049 fn dir_entries(
1050 &mut self,
1051 path: &Path,
1052 ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
1053 if let Self::Dir { entries, .. } = self {
1054 Ok(entries)
1055 } else {
1056 Err(anyhow!("not a directory: {}", path.display()))
1057 }
1058 }
1059}
1060
1061#[cfg(any(test, feature = "test-support"))]
1062#[async_trait::async_trait]
1063impl Fs for FakeFs {
1064 async fn create_dir(&self, path: &Path) -> Result<()> {
1065 self.simulate_random_delay().await;
1066
1067 let mut created_dirs = Vec::new();
1068 let mut cur_path = PathBuf::new();
1069 for component in path.components() {
1070 let mut state = self.state.lock();
1071 cur_path.push(component);
1072 if cur_path == Path::new("/") {
1073 continue;
1074 }
1075
1076 let inode = state.next_inode;
1077 let mtime = state.next_mtime;
1078 state.next_mtime += Duration::from_nanos(1);
1079 state.next_inode += 1;
1080 state.write_path(&cur_path, |entry| {
1081 entry.or_insert_with(|| {
1082 created_dirs.push(cur_path.clone());
1083 Arc::new(Mutex::new(FakeFsEntry::Dir {
1084 inode,
1085 mtime,
1086 entries: Default::default(),
1087 git_repo_state: None,
1088 }))
1089 });
1090 Ok(())
1091 })?
1092 }
1093
1094 self.state.lock().emit_event(&created_dirs);
1095 Ok(())
1096 }
1097
1098 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
1099 self.simulate_random_delay().await;
1100 let mut state = self.state.lock();
1101 let inode = state.next_inode;
1102 let mtime = state.next_mtime;
1103 state.next_mtime += Duration::from_nanos(1);
1104 state.next_inode += 1;
1105 let file = Arc::new(Mutex::new(FakeFsEntry::File {
1106 inode,
1107 mtime,
1108 content: Vec::new(),
1109 }));
1110 state.write_path(path, |entry| {
1111 match entry {
1112 btree_map::Entry::Occupied(mut e) => {
1113 if options.overwrite {
1114 *e.get_mut() = file;
1115 } else if !options.ignore_if_exists {
1116 return Err(anyhow!("path already exists: {}", path.display()));
1117 }
1118 }
1119 btree_map::Entry::Vacant(e) => {
1120 e.insert(file);
1121 }
1122 }
1123 Ok(())
1124 })?;
1125 state.emit_event([path]);
1126 Ok(())
1127 }
1128
1129 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
1130 let mut state = self.state.lock();
1131 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
1132 state
1133 .write_path(path.as_ref(), move |e| match e {
1134 btree_map::Entry::Vacant(e) => {
1135 e.insert(file);
1136 Ok(())
1137 }
1138 btree_map::Entry::Occupied(mut e) => {
1139 *e.get_mut() = file;
1140 Ok(())
1141 }
1142 })
1143 .unwrap();
1144 state.emit_event(&[path]);
1145 Ok(())
1146 }
1147
1148 async fn create_file_with(
1149 &self,
1150 path: &Path,
1151 mut content: Pin<&mut (dyn AsyncRead + Send)>,
1152 ) -> Result<()> {
1153 let mut bytes = Vec::new();
1154 content.read_to_end(&mut bytes).await?;
1155 self.write_file_internal(path, bytes)?;
1156 Ok(())
1157 }
1158
1159 async fn extract_tar_file(
1160 &self,
1161 path: &Path,
1162 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
1163 ) -> Result<()> {
1164 let mut entries = content.entries()?;
1165 while let Some(entry) = entries.next().await {
1166 let mut entry = entry?;
1167 if entry.header().entry_type().is_file() {
1168 let path = path.join(entry.path()?.as_ref());
1169 let mut bytes = Vec::new();
1170 entry.read_to_end(&mut bytes).await?;
1171 self.create_dir(path.parent().unwrap()).await?;
1172 self.write_file_internal(&path, bytes)?;
1173 }
1174 }
1175 Ok(())
1176 }
1177
1178 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
1179 self.simulate_random_delay().await;
1180
1181 let old_path = normalize_path(old_path);
1182 let new_path = normalize_path(new_path);
1183
1184 let mut state = self.state.lock();
1185 let moved_entry = state.write_path(&old_path, |e| {
1186 if let btree_map::Entry::Occupied(e) = e {
1187 Ok(e.get().clone())
1188 } else {
1189 Err(anyhow!("path does not exist: {}", &old_path.display()))
1190 }
1191 })?;
1192
1193 state.write_path(&new_path, |e| {
1194 match e {
1195 btree_map::Entry::Occupied(mut e) => {
1196 if options.overwrite {
1197 *e.get_mut() = moved_entry;
1198 } else if !options.ignore_if_exists {
1199 return Err(anyhow!("path already exists: {}", new_path.display()));
1200 }
1201 }
1202 btree_map::Entry::Vacant(e) => {
1203 e.insert(moved_entry);
1204 }
1205 }
1206 Ok(())
1207 })?;
1208
1209 state
1210 .write_path(&old_path, |e| {
1211 if let btree_map::Entry::Occupied(e) = e {
1212 Ok(e.remove())
1213 } else {
1214 unreachable!()
1215 }
1216 })
1217 .unwrap();
1218
1219 state.emit_event(&[old_path, new_path]);
1220 Ok(())
1221 }
1222
1223 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
1224 self.simulate_random_delay().await;
1225
1226 let source = normalize_path(source);
1227 let target = normalize_path(target);
1228 let mut state = self.state.lock();
1229 let mtime = state.next_mtime;
1230 let inode = util::post_inc(&mut state.next_inode);
1231 state.next_mtime += Duration::from_nanos(1);
1232 let source_entry = state.read_path(&source)?;
1233 let content = source_entry.lock().file_content(&source)?.clone();
1234 let entry = state.write_path(&target, |e| match e {
1235 btree_map::Entry::Occupied(e) => {
1236 if options.overwrite {
1237 Ok(Some(e.get().clone()))
1238 } else if !options.ignore_if_exists {
1239 return Err(anyhow!("{target:?} already exists"));
1240 } else {
1241 Ok(None)
1242 }
1243 }
1244 btree_map::Entry::Vacant(e) => Ok(Some(
1245 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
1246 inode,
1247 mtime,
1248 content: Vec::new(),
1249 })))
1250 .clone(),
1251 )),
1252 })?;
1253 if let Some(entry) = entry {
1254 entry.lock().set_file_content(&target, content)?;
1255 }
1256 state.emit_event(&[target]);
1257 Ok(())
1258 }
1259
1260 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1261 self.simulate_random_delay().await;
1262
1263 let path = normalize_path(path);
1264 let parent_path = path
1265 .parent()
1266 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1267 let base_name = path.file_name().unwrap();
1268
1269 let mut state = self.state.lock();
1270 let parent_entry = state.read_path(parent_path)?;
1271 let mut parent_entry = parent_entry.lock();
1272 let entry = parent_entry
1273 .dir_entries(parent_path)?
1274 .entry(base_name.to_str().unwrap().into());
1275
1276 match entry {
1277 btree_map::Entry::Vacant(_) => {
1278 if !options.ignore_if_not_exists {
1279 return Err(anyhow!("{path:?} does not exist"));
1280 }
1281 }
1282 btree_map::Entry::Occupied(e) => {
1283 {
1284 let mut entry = e.get().lock();
1285 let children = entry.dir_entries(&path)?;
1286 if !options.recursive && !children.is_empty() {
1287 return Err(anyhow!("{path:?} is not empty"));
1288 }
1289 }
1290 e.remove();
1291 }
1292 }
1293 state.emit_event(&[path]);
1294 Ok(())
1295 }
1296
1297 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1298 self.simulate_random_delay().await;
1299
1300 let path = normalize_path(path);
1301 let parent_path = path
1302 .parent()
1303 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1304 let base_name = path.file_name().unwrap();
1305 let mut state = self.state.lock();
1306 let parent_entry = state.read_path(parent_path)?;
1307 let mut parent_entry = parent_entry.lock();
1308 let entry = parent_entry
1309 .dir_entries(parent_path)?
1310 .entry(base_name.to_str().unwrap().into());
1311 match entry {
1312 btree_map::Entry::Vacant(_) => {
1313 if !options.ignore_if_not_exists {
1314 return Err(anyhow!("{path:?} does not exist"));
1315 }
1316 }
1317 btree_map::Entry::Occupied(e) => {
1318 e.get().lock().file_content(&path)?;
1319 e.remove();
1320 }
1321 }
1322 state.emit_event(&[path]);
1323 Ok(())
1324 }
1325
1326 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
1327 let bytes = self.load_internal(path).await?;
1328 Ok(Box::new(io::Cursor::new(bytes)))
1329 }
1330
1331 async fn load(&self, path: &Path) -> Result<String> {
1332 let content = self.load_internal(path).await?;
1333 Ok(String::from_utf8(content.clone())?)
1334 }
1335
1336 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
1337 self.simulate_random_delay().await;
1338 let path = normalize_path(path.as_path());
1339 self.write_file_internal(path, data.into_bytes())?;
1340 Ok(())
1341 }
1342
1343 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
1344 self.simulate_random_delay().await;
1345 let path = normalize_path(path);
1346 let content = chunks(text, line_ending).collect::<String>();
1347 if let Some(path) = path.parent() {
1348 self.create_dir(path).await?;
1349 }
1350 self.write_file_internal(path, content.into_bytes())?;
1351 Ok(())
1352 }
1353
1354 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
1355 let path = normalize_path(path);
1356 self.simulate_random_delay().await;
1357 let state = self.state.lock();
1358 if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
1359 Ok(canonical_path)
1360 } else {
1361 Err(anyhow!("path does not exist: {}", path.display()))
1362 }
1363 }
1364
1365 async fn is_file(&self, path: &Path) -> bool {
1366 let path = normalize_path(path);
1367 self.simulate_random_delay().await;
1368 let state = self.state.lock();
1369 if let Some((entry, _)) = state.try_read_path(&path, true) {
1370 entry.lock().is_file()
1371 } else {
1372 false
1373 }
1374 }
1375
1376 async fn is_dir(&self, path: &Path) -> bool {
1377 self.metadata(path)
1378 .await
1379 .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
1380 }
1381
1382 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
1383 self.simulate_random_delay().await;
1384 let path = normalize_path(path);
1385 let mut state = self.state.lock();
1386 state.metadata_call_count += 1;
1387 if let Some((mut entry, _)) = state.try_read_path(&path, false) {
1388 let is_symlink = entry.lock().is_symlink();
1389 if is_symlink {
1390 if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
1391 entry = e;
1392 } else {
1393 return Ok(None);
1394 }
1395 }
1396
1397 let entry = entry.lock();
1398 Ok(Some(match &*entry {
1399 FakeFsEntry::File { inode, mtime, .. } => Metadata {
1400 inode: *inode,
1401 mtime: *mtime,
1402 is_dir: false,
1403 is_symlink,
1404 },
1405 FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
1406 inode: *inode,
1407 mtime: *mtime,
1408 is_dir: true,
1409 is_symlink,
1410 },
1411 FakeFsEntry::Symlink { .. } => unreachable!(),
1412 }))
1413 } else {
1414 Ok(None)
1415 }
1416 }
1417
1418 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
1419 self.simulate_random_delay().await;
1420 let path = normalize_path(path);
1421 let state = self.state.lock();
1422 if let Some((entry, _)) = state.try_read_path(&path, false) {
1423 let entry = entry.lock();
1424 if let FakeFsEntry::Symlink { target } = &*entry {
1425 Ok(target.clone())
1426 } else {
1427 Err(anyhow!("not a symlink: {}", path.display()))
1428 }
1429 } else {
1430 Err(anyhow!("path does not exist: {}", path.display()))
1431 }
1432 }
1433
1434 async fn read_dir(
1435 &self,
1436 path: &Path,
1437 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
1438 self.simulate_random_delay().await;
1439 let path = normalize_path(path);
1440 let mut state = self.state.lock();
1441 state.read_dir_call_count += 1;
1442 let entry = state.read_path(&path)?;
1443 let mut entry = entry.lock();
1444 let children = entry.dir_entries(&path)?;
1445 let paths = children
1446 .keys()
1447 .map(|file_name| Ok(path.join(file_name)))
1448 .collect::<Vec<_>>();
1449 Ok(Box::pin(futures::stream::iter(paths)))
1450 }
1451
1452 async fn watch(
1453 &self,
1454 path: &Path,
1455 _: Duration,
1456 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
1457 self.simulate_random_delay().await;
1458 let (tx, rx) = smol::channel::unbounded();
1459 self.state.lock().event_txs.push(tx);
1460 let path = path.to_path_buf();
1461 let executor = self.executor.clone();
1462 Box::pin(futures::StreamExt::filter(rx, move |events| {
1463 let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
1464 let executor = executor.clone();
1465 async move {
1466 executor.simulate_random_delay().await;
1467 result
1468 }
1469 }))
1470 }
1471
1472 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
1473 let state = self.state.lock();
1474 let entry = state.read_path(abs_dot_git).unwrap();
1475 let mut entry = entry.lock();
1476 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
1477 let state = git_repo_state
1478 .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
1479 .clone();
1480 Some(git::repository::FakeGitRepository::open(state))
1481 } else {
1482 None
1483 }
1484 }
1485
1486 fn is_fake(&self) -> bool {
1487 true
1488 }
1489
1490 async fn is_case_sensitive(&self) -> Result<bool> {
1491 Ok(true)
1492 }
1493
1494 #[cfg(any(test, feature = "test-support"))]
1495 fn as_fake(&self) -> &FakeFs {
1496 self
1497 }
1498}
1499
1500fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
1501 rope.chunks().flat_map(move |chunk| {
1502 let mut newline = false;
1503 chunk.split('\n').flat_map(move |line| {
1504 let ending = if newline {
1505 Some(line_ending.as_str())
1506 } else {
1507 None
1508 };
1509 newline = true;
1510 ending.into_iter().chain([line])
1511 })
1512 })
1513}
1514
1515pub fn normalize_path(path: &Path) -> PathBuf {
1516 let mut components = path.components().peekable();
1517 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1518 components.next();
1519 PathBuf::from(c.as_os_str())
1520 } else {
1521 PathBuf::new()
1522 };
1523
1524 for component in components {
1525 match component {
1526 Component::Prefix(..) => unreachable!(),
1527 Component::RootDir => {
1528 ret.push(component.as_os_str());
1529 }
1530 Component::CurDir => {}
1531 Component::ParentDir => {
1532 ret.pop();
1533 }
1534 Component::Normal(c) => {
1535 ret.push(c);
1536 }
1537 }
1538 }
1539 ret
1540}
1541
1542pub fn copy_recursive<'a>(
1543 fs: &'a dyn Fs,
1544 source: &'a Path,
1545 target: &'a Path,
1546 options: CopyOptions,
1547) -> BoxFuture<'a, Result<()>> {
1548 use futures::future::FutureExt;
1549
1550 async move {
1551 let metadata = fs
1552 .metadata(source)
1553 .await?
1554 .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
1555 if metadata.is_dir {
1556 if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
1557 if options.ignore_if_exists {
1558 return Ok(());
1559 } else {
1560 return Err(anyhow!("{target:?} already exists"));
1561 }
1562 }
1563
1564 let _ = fs
1565 .remove_dir(
1566 target,
1567 RemoveOptions {
1568 recursive: true,
1569 ignore_if_not_exists: true,
1570 },
1571 )
1572 .await;
1573 fs.create_dir(target).await?;
1574 let mut children = fs.read_dir(source).await?;
1575 while let Some(child_path) = children.next().await {
1576 if let Ok(child_path) = child_path {
1577 if let Some(file_name) = child_path.file_name() {
1578 let child_target_path = target.join(file_name);
1579 copy_recursive(fs, &child_path, &child_target_path, options).await?;
1580 }
1581 }
1582 }
1583
1584 Ok(())
1585 } else {
1586 fs.copy_file(source, target, options).await
1587 }
1588 }
1589 .boxed()
1590}
1591
1592// todo(windows)
1593// can we get file id not open the file twice?
1594// https://github.com/rust-lang/rust/issues/63010
1595#[cfg(target_os = "windows")]
1596async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
1597 use std::os::windows::io::AsRawHandle;
1598
1599 use smol::fs::windows::OpenOptionsExt;
1600 use windows::Win32::{
1601 Foundation::HANDLE,
1602 Storage::FileSystem::{
1603 GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
1604 },
1605 };
1606
1607 let file = smol::fs::OpenOptions::new()
1608 .read(true)
1609 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
1610 .open(path)
1611 .await?;
1612
1613 let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
1614 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
1615 // This function supports Windows XP+
1616 smol::unblock(move || {
1617 unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
1618
1619 Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
1620 })
1621 .await
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use super::*;
1627 use gpui::BackgroundExecutor;
1628 use serde_json::json;
1629
1630 #[gpui::test]
1631 async fn test_fake_fs(executor: BackgroundExecutor) {
1632 let fs = FakeFs::new(executor.clone());
1633 fs.insert_tree(
1634 "/root",
1635 json!({
1636 "dir1": {
1637 "a": "A",
1638 "b": "B"
1639 },
1640 "dir2": {
1641 "c": "C",
1642 "dir3": {
1643 "d": "D"
1644 }
1645 }
1646 }),
1647 )
1648 .await;
1649
1650 assert_eq!(
1651 fs.files(),
1652 vec![
1653 PathBuf::from("/root/dir1/a"),
1654 PathBuf::from("/root/dir1/b"),
1655 PathBuf::from("/root/dir2/c"),
1656 PathBuf::from("/root/dir2/dir3/d"),
1657 ]
1658 );
1659
1660 fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
1661 .await
1662 .unwrap();
1663
1664 assert_eq!(
1665 fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
1666 .await
1667 .unwrap(),
1668 PathBuf::from("/root/dir2/dir3"),
1669 );
1670 assert_eq!(
1671 fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
1672 .await
1673 .unwrap(),
1674 PathBuf::from("/root/dir2/dir3/d"),
1675 );
1676 assert_eq!(
1677 fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
1678 "D",
1679 );
1680 }
1681}