Skip to content

Commit

Permalink
support for executable bit check (#301)
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Feb 28, 2022
1 parent d9b3efb commit 620c955
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 30 deletions.
53 changes: 37 additions & 16 deletions git-worktree/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
/// Common knowledge about the worktree that is needed across most interactions with the work tree
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
pub struct Context {
pub struct Capabilities {
/// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that
/// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally
/// using it. This also applies to input received from the command-line, so callers may have to be aware of this and
Expand All @@ -15,29 +15,50 @@ pub struct Context {
pub ignore_case: bool,
/// If true, we assume the the executable bit is honored as part of the files mode. If false, we assume the file system
/// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on.
pub file_mode: bool,
pub executable_bit: bool,
/// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked
/// out as files which contain the link as text.
pub symlink: bool,
}

impl Context {
impl Capabilities {
/// try to determine all values in this context by probing them in the given `git_dir`, which
/// should be on the file system the git repository is located on.
/// `git_dir` is a typical git repository, expected to be populated with the typical files like `config`.
///
/// All errors are ignored and interpreted on top of the default for the platform the binary is compiled for.
pub fn probe(git_dir: impl AsRef<std::path::Path>) -> Self {
pub fn probe(git_dir: impl AsRef<Path>) -> Self {
let root = git_dir.as_ref();
let ctx = Context::default();
Context {
let ctx = Capabilities::default();
Capabilities {
symlink: Self::probe_symlink(root).unwrap_or(ctx.symlink),
ignore_case: Self::probe_ignore_case(root).unwrap_or(ctx.ignore_case),
precompose_unicode: Self::probe_precompose_unicode(root).unwrap_or(ctx.precompose_unicode),
..ctx
executable_bit: Self::probe_file_mode(root).unwrap_or(ctx.executable_bit),
}
}

#[cfg(unix)]
fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};

// test it exactly as we typically create executable files, not using chmod.
let test_path = root.join("_test_executable_bit");
let res = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o777)
.open(&test_path)
.and_then(|f| f.metadata().map(|m| m.mode() & 0o100 == 0o100));
std::fs::remove_file(test_path)?;
res
}

#[cfg(not(unix))]
fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
Ok(false)
}

fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> {
std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
Expand Down Expand Up @@ -84,36 +105,36 @@ impl Context {
}

#[cfg(windows)]
impl Default for Context {
impl Default for Capabilities {
fn default() -> Self {
Context {
Capabilities {
precompose_unicode: false,
ignore_case: true,
file_mode: false,
executable_bit: false,
symlink: false,
}
}
}

#[cfg(target_os = "macos")]
impl Default for Context {
impl Default for Capabilities {
fn default() -> Self {
Context {
Capabilities {
precompose_unicode: true,
ignore_case: true,
file_mode: true,
executable_bit: true,
symlink: true,
}
}
}

#[cfg(all(unix, not(target_os = "macos")))]
impl Default for Context {
impl Default for Capabilities {
fn default() -> Self {
Context {
Capabilities {
precompose_unicode: false,
ignore_case: false,
file_mode: true,
executable_bit: true,
symlink: true,
}
}
Expand Down
25 changes: 14 additions & 11 deletions git-worktree/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub mod checkout {
#[derive(Default, Clone, Copy)]
pub struct Options {
/// capabilities of the file system
pub fs: crate::fs::Context,
pub fs: crate::fs::Capabilities,
/// If true, we assume no file to exist in the target directory, and want exclusive access to it.
/// This should be enabled when cloning.
pub destination_is_initially_empty: bool,
Expand Down Expand Up @@ -81,7 +81,12 @@ pub(crate) mod entry {
find: &mut Find,
root: &std::path::Path,
index::checkout::Options {
fs: crate::fs::Context { symlink, .. },
fs:
crate::fs::Capabilities {
symlink,
executable_bit,
..
},
..
}: index::checkout::Options,
buf: &mut Vec<u8>,
Expand All @@ -103,20 +108,18 @@ pub(crate) mod entry {
path: root.to_path_buf(),
})?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
options.create_new(true).write(true);
#[cfg(unix)]
if entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
if executable_bit && entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o777);
}

{
let mut file = options.open(&dest)?;
file.write_all(obj.data)?;
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
// revisit this once there is a bug to fix.
}
update_fstat(entry, dest.symlink_metadata()?)?;
let mut file = options.open(&dest)?;
file.write_all(obj.data)?;
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
// revisit this once there is a bug to fix.
update_fstat(entry, file.metadata()?)?;
}
git_index::entry::Mode::SYMLINK => {
let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound {
Expand Down
2 changes: 1 addition & 1 deletion git-worktree/tests/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
fn from_probing_cwd() {
let dir = tempfile::tempdir().unwrap();
std::fs::File::create(dir.path().join("config")).unwrap();
let ctx = git_worktree::fs::Context::probe(dir.path());
let ctx = git_worktree::fs::Capabilities::probe(dir.path());
dbg!(ctx);
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
Expand Down
4 changes: 2 additions & 2 deletions git-worktree/tests/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod checkout {
#[test]
fn allow_symlinks() -> crate::Result {
let opts = opts_with_symlink(true);
if !git_worktree::fs::Context::probe(std::env::current_dir()?.join("..").join(".git")).symlink {
if !git_worktree::fs::Capabilities::probe(std::env::current_dir()?.join("..").join(".git")).symlink {
eprintln!("IGNORING symlink test on file system without symlink support");
// skip if symlinks aren't supported anyway.
return Ok(());
Expand All @@ -30,7 +30,7 @@ mod checkout {

fn opts_with_symlink(symlink: bool) -> Options {
index::checkout::Options {
fs: git_worktree::fs::Context {
fs: git_worktree::fs::Capabilities {
symlink,
..Default::default()
},
Expand Down

0 comments on commit 620c955

Please sign in to comment.