2859 lines · 94.5 KB · Rust
RawDownload
#[cfg(target_os = "macos")]mod mac_watcher; #[cfg(not(target_os = "macos"))]pub mod fs_watcher; use anyhow::{Context as _, Result, anyhow};#[cfg(any(target_os = "linux", target_os = "freebsd"))]use ashpd::desktop::trash;use gpui::App;use gpui::BackgroundExecutor;use gpui::Global;use gpui::ReadGlobal as _;use std::borrow::Cow;use util::command::new_std_command; #[cfg(unix)]use std::os::fd::{AsFd, AsRawFd}; #[cfg(unix)]use std::os::unix::fs::{FileTypeExt, MetadataExt}; use async_tar::Archive;use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};use git::repository::{GitRepository, RealGitRepository};use rope::Rope;use serde::{Deserialize, Serialize};use smol::io::AsyncWriteExt;use std::{    io::{self, Write},    path::{Component, Path, PathBuf},    pin::Pin,    sync::Arc,    time::{Duration, SystemTime, UNIX_EPOCH},};use tempfile::{NamedTempFile, TempDir};use text::LineEnding; #[cfg(any(test, feature = "test-support"))]mod fake_git_repo;#[cfg(any(test, feature = "test-support"))]use collections::{BTreeMap, btree_map};#[cfg(any(test, feature = "test-support"))]use fake_git_repo::FakeGitRepositoryState;#[cfg(any(test, feature = "test-support"))]use git::{    repository::RepoPath,    status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},};#[cfg(any(test, feature = "test-support"))]use parking_lot::Mutex;#[cfg(any(test, feature = "test-support"))]use smol::io::AsyncReadExt;#[cfg(any(test, 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 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 home_dir(&self) -> Option<PathBuf>;    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;    fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;    fn is_fake(&self) -> bool;    async fn is_case_sensitive(&self) -> Result<bool>;     #[cfg(any(test, 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,} #[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,} /// 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); 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 {    git_binary_path: Option<PathBuf>,    executor: BackgroundExecutor,} 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: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];         let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };        if result == -1 {            anyhow::bail!("fcntl returned -1".to_string());        }         let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) };        let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));        Ok(path)    }     #[cfg(any(target_os = "linux", target_os = "freebsd"))]    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 = "windows")]    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {        anyhow::bail!("unimplemented")    }} pub struct RealWatcher {} impl RealFs {    pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {        Self {            git_binary_path,            executor,        }    }} #[async_trait::async_trait]impl Fs for RealFs {    async fn create_dir(&self, path: &Path) -> Result<()> {        Ok(smol::fs::create_dir_all(path).await?)    }     async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {        #[cfg(unix)]        smol::fs::unix::symlink(target, path).await?;         #[cfg(windows)]        if smol::fs::metadata(&target).await?.is_dir() {            smol::fs::windows::symlink_dir(target, path).await?        } else {            smol::fs::windows::symlink_file(target, path).await?        }         Ok(())    }     async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {        let mut open_options = smol::fs::OpenOptions::new();        open_options.write(true).create(true);        if options.overwrite {            open_options.truncate(true);        } else if !options.ignore_if_exists {            open_options.create_new(true);        }        open_options.open(path).await?;        Ok(())    }     async fn create_file_with(        &self,        path: &Path,        content: Pin<&mut (dyn AsyncRead + Send)>,    ) -> Result<()> {        let mut file = smol::fs::File::create(&path).await?;        futures::io::copy(content, &mut file).await?;        Ok(())    }     async fn extract_tar_file(        &self,        path: &Path,        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,    ) -> Result<()> {        content.unpack(path).await?;        Ok(())    }     async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {            if options.ignore_if_exists {                return Ok(());            } else {                return Err(anyhow!("{target:?} already exists"));            }        }         smol::fs::copy(source, target).await?;        Ok(())    }     async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {            if options.ignore_if_exists {                return Ok(());            } else {                return Err(anyhow!("{target:?} already exists"));            }        }         smol::fs::rename(source, target).await?;        Ok(())    }     async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {        let result = if options.recursive {            smol::fs::remove_dir_all(path).await        } else {            smol::fs::remove_dir(path).await        };        match result {            Ok(()) => Ok(()),            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {                Ok(())            }            Err(err) => Err(err)?,        }    }     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {        #[cfg(windows)]        if let Ok(Some(metadata)) = self.metadata(path).await {            if metadata.is_symlink && metadata.is_dir {                self.remove_dir(                    path,                    RemoveOptions {                        recursive: false,                        ignore_if_not_exists: true,                    },                )                .await?;                return Ok(());            }        }         match smol::fs::remove_file(path).await {            Ok(()) => Ok(()),            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {                Ok(())            }            Err(err) => Err(err)?,        }    }     #[cfg(target_os = "macos")]    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {        use cocoa::{            base::{id, nil},            foundation::{NSAutoreleasePool, NSString},        };        use objc::{class, msg_send, sel, sel_impl};         unsafe {            unsafe fn ns_string(string: &str) -> id {                unsafe { NSString::alloc(nil).init_str(string).autorelease() }            }             let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];            let array: id = msg_send![class!(NSArray), arrayWithObject: url];            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];             let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];        }        Ok(())    }     #[cfg(any(target_os = "linux", target_os = "freebsd"))]    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {        if let Ok(Some(metadata)) = self.metadata(path).await {            if metadata.is_symlink {                // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255                return self.remove_file(path, RemoveOptions::default()).await;            }        }        let file = smol::fs::File::open(path).await?;        match trash::trash_file(&file.as_fd()).await {            Ok(_) => Ok(()),            Err(err) => {                log::error!("Failed to trash file: {}", err);                // Trashing files can fail if you don't have a trashing dbus service configured.                // In that case, delete the file directly instead.                return self.remove_file(path, RemoveOptions::default()).await;            }        }    }     #[cfg(target_os = "windows")]    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {        use util::paths::SanitizedPath;        use windows::{            Storage::{StorageDeleteOption, StorageFile},            core::HSTRING,        };        // todo(windows)        // When new version of `windows-rs` release, make this operation `async`        let path = SanitizedPath::from(path.canonicalize()?);        let path_string = path.to_string();        let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;        file.DeleteAsync(StorageDeleteOption::Default)?.get()?;        Ok(())    }     #[cfg(target_os = "macos")]    async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {        self.trash_file(path, options).await    }     #[cfg(any(target_os = "linux", target_os = "freebsd"))]    async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {        self.trash_file(path, options).await    }     #[cfg(target_os = "windows")]    async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {        use util::paths::SanitizedPath;        use windows::{            Storage::{StorageDeleteOption, StorageFolder},            core::HSTRING,        };         // todo(windows)        // When new version of `windows-rs` release, make this operation `async`

Lines 1–500 of 2859.