Mercurial > public > mercurial-scm > hg-stable
diff rust/hg-core/src/checkexec.rs @ 49970: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 | |
children | 07792fd1837f |
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) +}