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.
--- /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)?;