Skip to content

Commit 620c955

Browse files
committed
support for executable bit check (#301)
1 parent d9b3efb commit 620c955

File tree

4 files changed

+54
-30
lines changed

4 files changed

+54
-30
lines changed

git-worktree/src/fs.rs

+37-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
/// Common knowledge about the worktree that is needed across most interactions with the work tree
44
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
55
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
6-
pub struct Context {
6+
pub struct Capabilities {
77
/// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that
88
/// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally
99
/// using it. This also applies to input received from the command-line, so callers may have to be aware of this and
@@ -15,29 +15,50 @@ pub struct Context {
1515
pub ignore_case: bool,
1616
/// If true, we assume the the executable bit is honored as part of the files mode. If false, we assume the file system
1717
/// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on.
18-
pub file_mode: bool,
18+
pub executable_bit: bool,
1919
/// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked
2020
/// out as files which contain the link as text.
2121
pub symlink: bool,
2222
}
2323

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

41+
#[cfg(unix)]
42+
fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
43+
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
44+
45+
// test it exactly as we typically create executable files, not using chmod.
46+
let test_path = root.join("_test_executable_bit");
47+
let res = std::fs::OpenOptions::new()
48+
.create_new(true)
49+
.write(true)
50+
.mode(0o777)
51+
.open(&test_path)
52+
.and_then(|f| f.metadata().map(|m| m.mode() & 0o100 == 0o100));
53+
std::fs::remove_file(test_path)?;
54+
res
55+
}
56+
57+
#[cfg(not(unix))]
58+
fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
59+
Ok(false)
60+
}
61+
4162
fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> {
4263
std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| {
4364
if err.kind() == std::io::ErrorKind::NotFound {
@@ -84,36 +105,36 @@ impl Context {
84105
}
85106

86107
#[cfg(windows)]
87-
impl Default for Context {
108+
impl Default for Capabilities {
88109
fn default() -> Self {
89-
Context {
110+
Capabilities {
90111
precompose_unicode: false,
91112
ignore_case: true,
92-
file_mode: false,
113+
executable_bit: false,
93114
symlink: false,
94115
}
95116
}
96117
}
97118

98119
#[cfg(target_os = "macos")]
99-
impl Default for Context {
120+
impl Default for Capabilities {
100121
fn default() -> Self {
101-
Context {
122+
Capabilities {
102123
precompose_unicode: true,
103124
ignore_case: true,
104-
file_mode: true,
125+
executable_bit: true,
105126
symlink: true,
106127
}
107128
}
108129
}
109130

110131
#[cfg(all(unix, not(target_os = "macos")))]
111-
impl Default for Context {
132+
impl Default for Capabilities {
112133
fn default() -> Self {
113-
Context {
134+
Capabilities {
114135
precompose_unicode: false,
115136
ignore_case: false,
116-
file_mode: true,
137+
executable_bit: true,
117138
symlink: true,
118139
}
119140
}

git-worktree/src/index.rs

+14-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod checkout {
77
#[derive(Default, Clone, Copy)]
88
pub struct Options {
99
/// capabilities of the file system
10-
pub fs: crate::fs::Context,
10+
pub fs: crate::fs::Capabilities,
1111
/// If true, we assume no file to exist in the target directory, and want exclusive access to it.
1212
/// This should be enabled when cloning.
1313
pub destination_is_initially_empty: bool,
@@ -81,7 +81,12 @@ pub(crate) mod entry {
8181
find: &mut Find,
8282
root: &std::path::Path,
8383
index::checkout::Options {
84-
fs: crate::fs::Context { symlink, .. },
84+
fs:
85+
crate::fs::Capabilities {
86+
symlink,
87+
executable_bit,
88+
..
89+
},
8590
..
8691
}: index::checkout::Options,
8792
buf: &mut Vec<u8>,
@@ -103,20 +108,18 @@ pub(crate) mod entry {
103108
path: root.to_path_buf(),
104109
})?;
105110
let mut options = OpenOptions::new();
106-
options.write(true).create_new(true);
111+
options.create_new(true).write(true);
107112
#[cfg(unix)]
108-
if entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
113+
if executable_bit && entry.mode == git_index::entry::Mode::FILE_EXECUTABLE {
109114
use std::os::unix::fs::OpenOptionsExt;
110115
options.mode(0o777);
111116
}
112117

113-
{
114-
let mut file = options.open(&dest)?;
115-
file.write_all(obj.data)?;
116-
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
117-
// revisit this once there is a bug to fix.
118-
}
119-
update_fstat(entry, dest.symlink_metadata()?)?;
118+
let mut file = options.open(&dest)?;
119+
file.write_all(obj.data)?;
120+
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
121+
// revisit this once there is a bug to fix.
122+
update_fstat(entry, file.metadata()?)?;
120123
}
121124
git_index::entry::Mode::SYMLINK => {
122125
let obj = find(&entry.id, buf).ok_or_else(|| index::checkout::Error::ObjectNotFound {

git-worktree/tests/fs/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
fn from_probing_cwd() {
33
let dir = tempfile::tempdir().unwrap();
44
std::fs::File::create(dir.path().join("config")).unwrap();
5-
let ctx = git_worktree::fs::Context::probe(dir.path());
5+
let ctx = git_worktree::fs::Capabilities::probe(dir.path());
66
dbg!(ctx);
77
let entries: Vec<_> = std::fs::read_dir(dir.path())
88
.unwrap()

git-worktree/tests/index/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod checkout {
1717
#[test]
1818
fn allow_symlinks() -> crate::Result {
1919
let opts = opts_with_symlink(true);
20-
if !git_worktree::fs::Context::probe(std::env::current_dir()?.join("..").join(".git")).symlink {
20+
if !git_worktree::fs::Capabilities::probe(std::env::current_dir()?.join("..").join(".git")).symlink {
2121
eprintln!("IGNORING symlink test on file system without symlink support");
2222
// skip if symlinks aren't supported anyway.
2323
return Ok(());
@@ -30,7 +30,7 @@ mod checkout {
3030

3131
fn opts_with_symlink(symlink: bool) -> Options {
3232
index::checkout::Options {
33-
fs: git_worktree::fs::Context {
33+
fs: git_worktree::fs::Capabilities {
3434
symlink,
3535
..Default::default()
3636
},

0 commit comments

Comments
 (0)