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