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