changeset 49894:678588b01af1

rhg: implement checkexec to support weird filesystems In particular, some of our repos are stored on a fileserver that simulates POSIX permissions poorly, in such a way that prevents the removal of execute permission. This causes rhg show a spurious unclean status, even though python hg reports the repo as clean. We fix this by making rhg implement the ~same checkexec logic that python hg does.
author Arseniy Alekseyev <aalekseyev@janestreet.com>
date Thu, 05 Jan 2023 17:15:03 +0000
parents 5f664401dd03
children 07792fd1837f
files rust/hg-core/src/checkexec.rs rust/hg-core/src/lib.rs rust/rhg/src/commands/status.rs
diffstat 3 files changed, 134 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/checkexec.rs	Thu Jan 05 17:15:03 2023 +0000
@@ -0,0 +1,111 @@
+use std::fs;
+use std::io;
+use std::os::unix::fs::{MetadataExt, PermissionsExt};
+use std::path::Path;
+
+// This is a rust rewrite of [checkexec] function from [posix.py]
+
+const EXECFLAGS: u32 = 0o111;
+
+fn is_executable(path: impl AsRef<Path>) -> Result<bool, io::Error> {
+    let metadata = fs::metadata(path)?;
+    let mode = metadata.mode();
+    Ok(mode & EXECFLAGS != 0)
+}
+
+fn make_executable(path: impl AsRef<Path>) -> Result<(), io::Error> {
+    let mode = fs::metadata(path.as_ref())?.mode();
+    fs::set_permissions(
+        path,
+        fs::Permissions::from_mode((mode & 0o777) | EXECFLAGS),
+    )?;
+    Ok(())
+}
+
+fn copy_mode(
+    src: impl AsRef<Path>,
+    dst: impl AsRef<Path>,
+) -> Result<(), io::Error> {
+    let mode = match fs::symlink_metadata(src) {
+        Ok(metadata) => metadata.mode(),
+        Err(e) if e.kind() == io::ErrorKind::NotFound =>
+        // copymode in python has a more complicated handling of FileNotFound
+        // error, which we don't need because all it does is applying
+        // umask, which the OS already does when we mkdir.
+        {
+            return Ok(())
+        }
+        Err(e) => return Err(e),
+    };
+    fs::set_permissions(dst, fs::Permissions::from_mode(mode))?;
+    Ok(())
+}
+
+fn check_exec_impl(path: impl AsRef<Path>) -> Result<bool, io::Error> {
+    let basedir = path.as_ref().join(".hg");
+    let cachedir = basedir.join("wcache");
+    let storedir = basedir.join("store");
+
+    if !cachedir.exists() {
+        fs::create_dir(&cachedir)
+            .and_then(|()| {
+                if storedir.exists() {
+                    copy_mode(&storedir, &cachedir)
+                } else {
+                    copy_mode(&basedir, &cachedir)
+                }
+            })
+            .ok();
+    }
+
+    let leave_file: bool;
+    let checkdir: &Path;
+    let checkisexec = cachedir.join("checkisexec");
+    let checknoexec = cachedir.join("checknoexec");
+    if cachedir.is_dir() {
+        match is_executable(&checkisexec) {
+            Err(e) if e.kind() == io::ErrorKind::NotFound => (),
+            Err(e) => return Err(e),
+            Ok(is_exec) => {
+                if is_exec {
+                    let noexec_is_exec = match is_executable(&checknoexec) {
+                        Err(e) if e.kind() == io::ErrorKind::NotFound => {
+                            fs::write(&checknoexec, "")?;
+                            is_executable(&checknoexec)?
+                        }
+                        Err(e) => return Err(e),
+                        Ok(exec) => exec,
+                    };
+                    if !noexec_is_exec {
+                        // check-exec is exec and check-no-exec is not exec
+                        return Ok(true);
+                    }
+                    fs::remove_file(&checknoexec)?;
+                }
+                fs::remove_file(&checkisexec)?;
+            }
+        }
+        checkdir = &cachedir;
+        leave_file = true;
+    } else {
+        checkdir = path.as_ref();
+        leave_file = false;
+    };
+
+    let tmp_file = tempfile::NamedTempFile::new_in(checkdir)?;
+    if !is_executable(tmp_file.path())? {
+        make_executable(tmp_file.path())?;
+        if is_executable(tmp_file.path())? {
+            if leave_file {
+                tmp_file.persist(checkisexec).ok();
+            }
+            return Ok(true);
+        }
+    }
+
+    Ok(false)
+}
+
+pub fn check_exec(path: impl AsRef<Path>) -> bool {
+    check_exec_impl(path).unwrap_or(false)
+}
--- a/rust/hg-core/src/lib.rs	Wed Jan 11 16:16:06 2023 +0000
+++ b/rust/hg-core/src/lib.rs	Thu Jan 05 17:15:03 2023 +0000
@@ -30,6 +30,7 @@
 pub mod repo;
 pub mod revlog;
 pub use revlog::*;
+pub mod checkexec;
 pub mod config;
 pub mod lock;
 pub mod logging;
--- a/rust/rhg/src/commands/status.rs	Wed Jan 11 16:16:06 2023 +0000
+++ b/rust/rhg/src/commands/status.rs	Thu Jan 05 17:15:03 2023 +0000
@@ -254,10 +254,10 @@
 
     let mut dmap = repo.dirstate_map_mut()?;
 
+    let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
+
     let options = StatusOptions {
-        // we're currently supporting file systems with exec flags only
-        // anyway
-        check_exec: true,
+        check_exec,
         list_clean: display_states.clean,
         list_unknown: display_states.unknown,
         list_ignored: display_states.ignored,
@@ -312,6 +312,7 @@
                     unsure_is_modified(
                         working_directory_vfs,
                         store_vfs,
+                        check_exec,
                         &manifest,
                         &to_check.path,
                     )
@@ -554,6 +555,7 @@
 fn unsure_is_modified(
     working_directory_vfs: hg::vfs::Vfs,
     store_vfs: hg::vfs::Vfs,
+    check_exec: bool,
     manifest: &Manifest,
     hg_path: &HgPath,
 ) -> Result<bool, HgError> {
@@ -561,20 +563,32 @@
     let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
     let fs_metadata = vfs.symlink_metadata(&fs_path)?;
     let is_symlink = fs_metadata.file_type().is_symlink();
+
+    let entry = manifest
+        .find_by_path(hg_path)?
+        .expect("ambgious file not in p1");
+
     // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
     // dirstate
     let fs_flags = if is_symlink {
         Some(b'l')
-    } else if has_exec_bit(&fs_metadata) {
+    } else if check_exec && has_exec_bit(&fs_metadata) {
         Some(b'x')
     } else {
         None
     };
 
-    let entry = manifest
-        .find_by_path(hg_path)?
-        .expect("ambgious file not in p1");
-    if entry.flags != fs_flags {
+    let entry_flags = if check_exec {
+        entry.flags
+    } else {
+        if entry.flags == Some(b'x') {
+            None
+        } else {
+            entry.flags
+        }
+    };
+
+    if entry_flags != fs_flags {
         return Ok(true);
     }
     let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;