# HG changeset patch # User Arseniy Alekseyev # Date 2023-01-05 17:15:03 # Node ID 678588b01af129b62f3d2c6f2e911029e59942ab # Parent 5f664401dd034afea6a7f68be825b7fe2008c8aa 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. diff --git a/rust/hg-core/src/checkexec.rs b/rust/hg-core/src/checkexec.rs new file mode 100644 --- /dev/null +++ b/rust/hg-core/src/checkexec.rs @@ -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) -> Result { + let metadata = fs::metadata(path)?; + let mode = metadata.mode(); + Ok(mode & EXECFLAGS != 0) +} + +fn make_executable(path: impl AsRef) -> 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, + dst: impl AsRef, +) -> 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) -> Result { + 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) -> bool { + check_exec_impl(path).unwrap_or(false) +} diff --git a/rust/hg-core/src/lib.rs b/rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs +++ b/rust/hg-core/src/lib.rs @@ -30,6 +30,7 @@ pub mod matchers; pub mod repo; pub mod revlog; pub use revlog::*; +pub mod checkexec; pub mod config; pub mod lock; pub mod logging; diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs +++ b/rust/rhg/src/commands/status.rs @@ -254,10 +254,10 @@ pub fn run(invocation: &crate::CliInvoca 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 @@ pub fn run(invocation: &crate::CliInvoca unsure_is_modified( working_directory_vfs, store_vfs, + check_exec, &manifest, &to_check.path, ) @@ -554,6 +555,7 @@ impl DisplayStatusPaths<'_> { fn unsure_is_modified( working_directory_vfs: hg::vfs::Vfs, store_vfs: hg::vfs::Vfs, + check_exec: bool, manifest: &Manifest, hg_path: &HgPath, ) -> Result { @@ -561,20 +563,32 @@ fn unsure_is_modified( 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)?;