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