pub mod fs_watcher; use parking_lot::Mutex;use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};use std::time::Instant;use util::maybe; use anyhow::{Context as _, Result, anyhow};#[cfg(any(target_os = "linux", target_os = "freebsd"))]use ashpd::desktop::trash;use futures::stream::iter;use gpui::App;use gpui::BackgroundExecutor;use gpui::Global;use gpui::ReadGlobal as _;use gpui::SharedString;use std::borrow::Cow;#[cfg(unix)]use std::ffi::CString;use util::command::new_command; #[cfg(unix)]use std::os::fd::{AsFd, AsRawFd};#[cfg(unix)]use std::os::unix::ffi::OsStrExt; #[cfg(unix)]use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(any(target_os = "macos", target_os = "freebsd"))]use std::mem::MaybeUninit; use async_tar::Archive;use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};use git::repository::{GitRepository, RealGitRepository};use is_executable::IsExecutable;use rope::Rope;use serde::{Deserialize, Serialize};use smol::io::AsyncWriteExt;#[cfg(feature = "test-support")]use std::path::Component;use std::{ io::{self, Write}, path::{Path, PathBuf}, pin::Pin, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH},};use tempfile::TempDir;use text::LineEnding; #[cfg(feature = "test-support")]mod fake_git_repo;#[cfg(feature = "test-support")]use collections::{BTreeMap, btree_map};#[cfg(feature = "test-support")]use fake_git_repo::FakeGitRepositoryState;#[cfg(feature = "test-support")]use git::{ repository::{InitialGraphCommitData, RepoPath, repo_path}, status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},}; #[cfg(feature = "test-support")]use smol::io::AsyncReadExt;#[cfg(feature = "test-support")]use std::ffi::OsStr; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>;} #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]pub enum PathEventKind { Removed, Created, Changed,} #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]pub struct PathEvent { pub path: PathBuf, pub kind: Option<PathEventKind>,} impl From<PathEvent> for PathBuf { fn from(event: PathEvent) -> Self { event.path }} #[async_trait::async_trait]pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()>; async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()>; async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_dir(path, options).await } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_file(path, options).await } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>; async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>>; async fn load(&self, path: &Path) -> Result<String> { Ok(String::from_utf8(self.load_bytes(path).await?)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn write(&self, path: &Path, content: &[u8]) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn is_file(&self, path: &Path) -> bool; async fn is_dir(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>; async fn read_link(&self, path: &Path) -> Result<PathBuf>; async fn read_dir( &self, path: &Path, ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>; async fn watch( &self, path: &Path, latency: Duration, ) -> ( Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>, Arc<dyn Watcher>, ); fn open_repo( &self, abs_dot_git: &Path, system_git_binary_path: Option<&Path>, ) -> Result<Arc<dyn GitRepository>>; async fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>; async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> bool; fn subscribe_to_jobs(&self) -> JobEventReceiver; #[cfg(feature = "test-support")] fn as_fake(&self) -> Arc<FakeFs> { panic!("called as_fake on a real fs"); }} struct GlobalFs(Arc<dyn Fs>); impl Global for GlobalFs {} impl dyn Fs { /// Returns the global [`Fs`]. pub fn global(cx: &App) -> Arc<Self> { GlobalFs::global(cx).0.clone() } /// Sets the global [`Fs`]. pub fn set_global(fs: Arc<Self>, cx: &mut App) { cx.set_global(GlobalFs(fs)); }} #[derive(Copy, Clone, Default)]pub struct CreateOptions { pub overwrite: bool, pub ignore_if_exists: bool,} #[derive(Copy, Clone, Default)]pub struct CopyOptions { pub overwrite: bool, pub ignore_if_exists: bool,} #[derive(Copy, Clone, Default)]pub struct RenameOptions { pub overwrite: bool, pub ignore_if_exists: bool, /// Whether to create parent directories if they do not exist. pub create_parents: bool,} #[derive(Copy, Clone, Default)]pub struct RemoveOptions { pub recursive: bool, pub ignore_if_not_exists: bool,} #[derive(Copy, Clone, Debug)]pub struct Metadata { pub inode: u64, pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool, pub is_executable: bool,} /// Filesystem modification time. The purpose of this newtype is to discourage use of operations/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).////// Do not derive Ord, PartialOrd, or arithmetic operation traits.#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]#[serde(transparent)]pub struct MTime(SystemTime); pub type JobId = usize; #[derive(Clone, Debug)]pub struct JobInfo { pub start: Instant, pub message: SharedString, pub id: JobId,} #[derive(Debug, Clone)]pub enum JobEvent { Started { info: JobInfo }, Completed { id: JobId },} pub type JobEventSender = futures::channel::mpsc::UnboundedSender<JobEvent>;pub type JobEventReceiver = futures::channel::mpsc::UnboundedReceiver<JobEvent>; struct JobTracker { id: JobId, subscribers: Arc<Mutex<Vec<JobEventSender>>>,} impl JobTracker { fn new(info: JobInfo, subscribers: Arc<Mutex<Vec<JobEventSender>>>) -> Self { let id = info.id; { let mut subs = subscribers.lock(); subs.retain(|sender| { sender .unbounded_send(JobEvent::Started { info: info.clone() }) .is_ok() }); } Self { id, subscribers } }} impl Drop for JobTracker { fn drop(&mut self) { let mut subs = self.subscribers.lock(); subs.retain(|sender| { sender .unbounded_send(JobEvent::Completed { id: self.id }) .is_ok() }); }} impl MTime { /// Conversion intended for persistence and testing. pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { MTime(UNIX_EPOCH + Duration::new(secs, nanos)) } /// Conversion intended for persistence. pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { self.0 .duration_since(UNIX_EPOCH) .ok() .map(|duration| (duration.as_secs(), duration.subsec_nanos())) } /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including /// "_for_user" is to discourage misuse - this method should not be used when making decisions /// about file dirtiness. pub fn timestamp_for_user(self) -> SystemTime { self.0 } /// Temporary method to split out the behavior changes from introduction of this newtype. pub fn bad_is_greater_than(self, other: MTime) -> bool { self.0 > other.0 }} impl From<proto::Timestamp> for MTime { fn from(timestamp: proto::Timestamp) -> Self { MTime(timestamp.into()) }} impl From<MTime> for proto::Timestamp { fn from(mtime: MTime) -> Self { mtime.0.into() }} pub struct RealFs { bundled_git_binary_path: Option<PathBuf>, executor: BackgroundExecutor, next_job_id: Arc<AtomicUsize>, job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>, is_case_sensitive: AtomicU8,} pub trait FileHandle: Send + Sync + std::fmt::Debug { fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>;} impl FileHandle for std::fs::File { #[cfg(target_os = "macos")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit(); let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) }; anyhow::ensure!(result != -1, "fcntl returned -1"); // SAFETY: `fcntl` will initialize the path buffer. let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) }; anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle"); let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(target_os = "linux")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { let fd = self.as_fd(); let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd()); let new_path = std::fs::read_link(fd_path)?; if new_path .file_name() .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)")) { anyhow::bail!("file was deleted") }; Ok(new_path) } #[cfg(target_os = "freebsd")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut kif = MaybeUninit::<libc::kinfo_file>::uninit(); kif.kf_structsize = libc::KINFO_FILE_SIZE; let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) }; anyhow::ensure!(result != -1, "fcntl returned -1"); // SAFETY: `fcntl` will initialize the kif. let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) }; anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle"); let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(target_os = "windows")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use std::os::windows::io::AsRawHandle; use windows::Win32::Foundation::HANDLE; use windows::Win32::Storage::FileSystem::{ FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW, }; let handle = HANDLE(self.as_raw_handle() as _); // Query required buffer size (in wide chars) let required_len = unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) }; anyhow::ensure!( required_len != 0, "GetFinalPathNameByHandleW returned 0 length" ); // Allocate buffer and retrieve the path let mut buf: Vec<u16> = vec![0u16; required_len as usize + 1]; let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) }; anyhow::ensure!( written != 0, "GetFinalPathNameByHandleW failed to write path" ); let os_str: OsString = OsString::from_wide(&buf[..written as usize]); anyhow::ensure!(!os_str.is_empty(), "Could find a path for the file handle"); Ok(PathBuf::from(os_str)) }} pub struct RealWatcher {} impl RealFs { pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self { Self { bundled_git_binary_path: git_binary_path, executor, next_job_id: Arc::new(AtomicUsize::new(0)), job_event_subscribers: Arc::new(Mutex::new(Vec::new())), is_case_sensitive: Default::default(), } } #[cfg(target_os = "windows")] fn canonicalize(path: &Path) -> Result<PathBuf> { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use windows::Win32::Storage::FileSystem::GetVolumePathNameW; use windows::core::HSTRING; // std::fs::canonicalize resolves mapped network paths to UNC paths, which can // confuse some software. To mitigate this, we canonicalize the input, then rebase // the result onto the input's original volume root if both paths are on the same // volume. This keeps the same drive letter or mount point the caller used. let abs_path = if path.is_relative() { std::env::current_dir()?.join(path) } else { path.to_path_buf() }; let path_hstring = HSTRING::from(abs_path.as_os_str()); let mut vol_buf = vec![0u16; abs_path.as_os_str().len() + 2]; unsafe { GetVolumePathNameW(&path_hstring, &mut vol_buf)? }; let volume_root = { let len = vol_buf .iter() .position(|&c| c == 0) .unwrap_or(vol_buf.len()); PathBuf::from(OsString::from_wide(&vol_buf[..len])) }; let resolved_path = dunce::canonicalize(&abs_path)?; let resolved_root = dunce::canonicalize(&volume_root)?; if let Ok(relative) = resolved_path.strip_prefix(&resolved_root) { let mut result = volume_root; result.push(relative); Ok(result) } else { Ok(resolved_path) } }} #[cfg(any(target_os = "macos", target_os = "linux"))]fn rename_without_replace(source: &Path, target: &Path) -> io::Result<()> { let source = path_to_c_string(source)?; let target = path_to_c_string(target)?; #[cfg(target_os = "macos")] let result = unsafe { libc::renamex_np(source.as_ptr(), target.as_ptr(), libc::RENAME_EXCL) }; #[cfg(target_os = "linux")] let result = unsafe { libc::syscall( libc::SYS_renameat2, libc::AT_FDCWD, source.as_ptr(), libc::AT_FDCWD, target.as_ptr(), libc::RENAME_NOREPLACE, ) }; if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) }} Lines 1–500 of 3017. Load more
pub mod fs_watcher; use parking_lot::Mutex; use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; use std::time::Instant; use util::maybe; use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; use futures::stream::iter; use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; use gpui::ReadGlobal as _; use gpui::SharedString; use std::borrow::Cow; #[cfg(unix)] use std::ffi::CString; use util::command::new_command; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(any(target_os = "macos", target_os = "freebsd"))] use std::mem::MaybeUninit; use async_tar::Archive; use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture}; use git::repository::{GitRepository, RealGitRepository}; use is_executable::IsExecutable; use rope::Rope; use serde::{Deserialize, Serialize}; use smol::io::AsyncWriteExt; #[cfg(feature = "test-support")] use std::path::Component; use std::{ io::{self, Write}, path::{Path, PathBuf}, pin::Pin, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::TempDir; use text::LineEnding; #[cfg(feature = "test-support")] mod fake_git_repo; #[cfg(feature = "test-support")] use collections::{BTreeMap, btree_map}; #[cfg(feature = "test-support")] use fake_git_repo::FakeGitRepositoryState; #[cfg(feature = "test-support")] use git::{ repository::{InitialGraphCommitData, RepoPath, repo_path}, status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; #[cfg(feature = "test-support")] use smol::io::AsyncReadExt; #[cfg(feature = "test-support")] use std::ffi::OsStr; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>; } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum PathEventKind { Removed, Created, Changed, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct PathEvent { pub path: PathBuf, pub kind: Option<PathEventKind>, } impl From<PathEvent> for PathBuf { fn from(event: PathEvent) -> Self { event.path } } #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()>; async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()>; async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_dir(path, options).await } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_file(path, options).await } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>; async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>>; async fn load(&self, path: &Path) -> Result<String> { Ok(String::from_utf8(self.load_bytes(path).await?)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn write(&self, path: &Path, content: &[u8]) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn is_file(&self, path: &Path) -> bool; async fn is_dir(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>; async fn read_link(&self, path: &Path) -> Result<PathBuf>; async fn read_dir( &self, path: &Path, ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>; async fn watch( &self, path: &Path, latency: Duration, ) -> ( Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>, Arc<dyn Watcher>, ); fn open_repo( &self, abs_dot_git: &Path, system_git_binary_path: Option<&Path>, ) -> Result<Arc<dyn GitRepository>>; async fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>; async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> bool; fn subscribe_to_jobs(&self) -> JobEventReceiver; #[cfg(feature = "test-support")] fn as_fake(&self) -> Arc<FakeFs> { panic!("called as_fake on a real fs"); } } struct GlobalFs(Arc<dyn Fs>); impl Global for GlobalFs {} impl dyn Fs { /// Returns the global [`Fs`]. pub fn global(cx: &App) -> Arc<Self> { GlobalFs::global(cx).0.clone() } /// Sets the global [`Fs`]. pub fn set_global(fs: Arc<Self>, cx: &mut App) { cx.set_global(GlobalFs(fs)); } } #[derive(Copy, Clone, Default)] pub struct CreateOptions { pub overwrite: bool, pub ignore_if_exists: bool, } #[derive(Copy, Clone, Default)] pub struct CopyOptions { pub overwrite: bool, pub ignore_if_exists: bool, } #[derive(Copy, Clone, Default)] pub struct RenameOptions { pub overwrite: bool, pub ignore_if_exists: bool, /// Whether to create parent directories if they do not exist. pub create_parents: bool, } #[derive(Copy, Clone, Default)] pub struct RemoveOptions { pub recursive: bool, pub ignore_if_not_exists: bool, } #[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool, pub is_executable: bool, } /// Filesystem modification time. The purpose of this newtype is to discourage use of operations /// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using /// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it /// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113). /// /// Do not derive Ord, PartialOrd, or arithmetic operation traits. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] pub struct MTime(SystemTime); pub type JobId = usize; #[derive(Clone, Debug)] pub struct JobInfo { pub start: Instant, pub message: SharedString, pub id: JobId, } #[derive(Debug, Clone)] pub enum JobEvent { Started { info: JobInfo }, Completed { id: JobId }, } pub type JobEventSender = futures::channel::mpsc::UnboundedSender<JobEvent>; pub type JobEventReceiver = futures::channel::mpsc::UnboundedReceiver<JobEvent>; struct JobTracker { id: JobId, subscribers: Arc<Mutex<Vec<JobEventSender>>>, } impl JobTracker { fn new(info: JobInfo, subscribers: Arc<Mutex<Vec<JobEventSender>>>) -> Self { let id = info.id; { let mut subs = subscribers.lock(); subs.retain(|sender| { sender .unbounded_send(JobEvent::Started { info: info.clone() }) .is_ok() }); } Self { id, subscribers } } } impl Drop for JobTracker { fn drop(&mut self) { let mut subs = self.subscribers.lock(); subs.retain(|sender| { sender .unbounded_send(JobEvent::Completed { id: self.id }) .is_ok() }); } } impl MTime { /// Conversion intended for persistence and testing. pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { MTime(UNIX_EPOCH + Duration::new(secs, nanos)) } /// Conversion intended for persistence. pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { self.0 .duration_since(UNIX_EPOCH) .ok() .map(|duration| (duration.as_secs(), duration.subsec_nanos())) } /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including /// "_for_user" is to discourage misuse - this method should not be used when making decisions /// about file dirtiness. pub fn timestamp_for_user(self) -> SystemTime { self.0 } /// Temporary method to split out the behavior changes from introduction of this newtype. pub fn bad_is_greater_than(self, other: MTime) -> bool { self.0 > other.0 } } impl From<proto::Timestamp> for MTime { fn from(timestamp: proto::Timestamp) -> Self { MTime(timestamp.into()) } } impl From<MTime> for proto::Timestamp { fn from(mtime: MTime) -> Self { mtime.0.into() } } pub struct RealFs { bundled_git_binary_path: Option<PathBuf>, executor: BackgroundExecutor, next_job_id: Arc<AtomicUsize>, job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>, is_case_sensitive: AtomicU8, } pub trait FileHandle: Send + Sync + std::fmt::Debug { fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>; } impl FileHandle for std::fs::File { #[cfg(target_os = "macos")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit(); let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) }; anyhow::ensure!(result != -1, "fcntl returned -1"); // SAFETY: `fcntl` will initialize the path buffer. let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) }; anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle"); let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(target_os = "linux")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { let fd = self.as_fd(); let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd()); let new_path = std::fs::read_link(fd_path)?; if new_path .file_name() .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)")) { anyhow::bail!("file was deleted") }; Ok(new_path) } #[cfg(target_os = "freebsd")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut kif = MaybeUninit::<libc::kinfo_file>::uninit(); kif.kf_structsize = libc::KINFO_FILE_SIZE; let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) }; anyhow::ensure!(result != -1, "fcntl returned -1"); // SAFETY: `fcntl` will initialize the kif. let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) }; anyhow::ensure!(!c_str.is_empty(), "Could find a path for the file handle"); let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(target_os = "windows")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use std::os::windows::io::AsRawHandle; use windows::Win32::Foundation::HANDLE; use windows::Win32::Storage::FileSystem::{ FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW, }; let handle = HANDLE(self.as_raw_handle() as _); // Query required buffer size (in wide chars) let required_len = unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) }; anyhow::ensure!( required_len != 0, "GetFinalPathNameByHandleW returned 0 length" ); // Allocate buffer and retrieve the path let mut buf: Vec<u16> = vec![0u16; required_len as usize + 1]; let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) }; anyhow::ensure!( written != 0, "GetFinalPathNameByHandleW failed to write path" ); let os_str: OsString = OsString::from_wide(&buf[..written as usize]); anyhow::ensure!(!os_str.is_empty(), "Could find a path for the file handle"); Ok(PathBuf::from(os_str)) } } pub struct RealWatcher {} impl RealFs { pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self { Self { bundled_git_binary_path: git_binary_path, executor, next_job_id: Arc::new(AtomicUsize::new(0)), job_event_subscribers: Arc::new(Mutex::new(Vec::new())), is_case_sensitive: Default::default(), } } #[cfg(target_os = "windows")] fn canonicalize(path: &Path) -> Result<PathBuf> { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use windows::Win32::Storage::FileSystem::GetVolumePathNameW; use windows::core::HSTRING; // std::fs::canonicalize resolves mapped network paths to UNC paths, which can // confuse some software. To mitigate this, we canonicalize the input, then rebase // the result onto the input's original volume root if both paths are on the same // volume. This keeps the same drive letter or mount point the caller used. let abs_path = if path.is_relative() { std::env::current_dir()?.join(path) } else { path.to_path_buf() }; let path_hstring = HSTRING::from(abs_path.as_os_str()); let mut vol_buf = vec![0u16; abs_path.as_os_str().len() + 2]; unsafe { GetVolumePathNameW(&path_hstring, &mut vol_buf)? }; let volume_root = { let len = vol_buf .iter() .position(|&c| c == 0) .unwrap_or(vol_buf.len()); PathBuf::from(OsString::from_wide(&vol_buf[..len])) }; let resolved_path = dunce::canonicalize(&abs_path)?; let resolved_root = dunce::canonicalize(&volume_root)?; if let Ok(relative) = resolved_path.strip_prefix(&resolved_root) { let mut result = volume_root; result.push(relative); Ok(result) } else { Ok(resolved_path) } } } #[cfg(any(target_os = "macos", target_os = "linux"))] fn rename_without_replace(source: &Path, target: &Path) -> io::Result<()> { let source = path_to_c_string(source)?; let target = path_to_c_string(target)?; #[cfg(target_os = "macos")] let result = unsafe { libc::renamex_np(source.as_ptr(), target.as_ptr(), libc::RENAME_EXCL) }; #[cfg(target_os = "linux")] let result = unsafe { libc::syscall( libc::SYS_renameat2, libc::AT_FDCWD, source.as_ptr(), libc::AT_FDCWD, target.as_ptr(), libc::RENAME_NOREPLACE, ) }; if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } }